原子性与可见性

Java 内存模型是围绕着原子性、可见性、有序性三个方面建立的,理解原子性与可见性有助于我们理解 Java 内存模型,加深对多线程编程的理解。


原子性

原子性指一个操作不能被打断,要么全部执行完毕,要么不执行。

除了 long 和 double 型变量,java 内存模型确保访问任意类型变量所对应的内存单元都是原子的,包括引用类型的字段。long 和 double 类型的变量是64位。在32位 JVM 中,64位数据的读写操作会分为2次32位的读写操作来进行,因此 long、double 类型的变量在32位虚拟机中是非原子操作(在64位 JVM 下具有原子性)。

原子性问题只存在于对实例变量、静态变量、数组元素的读写操作,不包括局部变量。

判断下面哪些是原子操作?

int i = 10;
long j = 2L;
int t = i;
i++;
return i

答案:

  1. 原子性操作
  2. 分情况讨论
  3. 非原子性
  4. 非原子性
  5. 原子性

语句3需要两步操作:获取i的值 -> 赋值给t;语句4也有两步操作:获取i的值->加1。因此,只有 return 与基本变量的直接赋值(long、double 除外)是保证原子性的,对于 long、double 型变量可以通过 volatile 修饰符来保证原子性。

为什么需要原子性

如果只是用顺序编程的话,自然不需要考虑这些问题,但事实上多线程编程已经被广泛使用。如果此时某些操作不能保证原子性的话,可能会发生意想不到的错误。

下面这个程序会不会输出?

public class AtomicTest implements Runnable {
    private int i = 0;

    public int getValue() {
        return i;
    }

    private synchronized void increment() {
        i++;
        i++;
    }

    public void run() {
        while (true) {
            increment();
        }
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        AtomicTest test = new AtomicTest();
        exec.execute(test);
        while (true) {
            int t = test.getValue();
            if (t % 2 != 0) {
                System.out.println(t);
                System.exit(0);
            }
        }
    }
}

答案是这个程序有可能输出奇数值并终止,尤其在每次启动的时候,有更大几率发生。

会输出奇数值的原因在于 increment() 中的操作是非原子性的,因此 getValue() 方法有可能获取的是不稳定的中间状态。此外,i 也没有用 volatile 修饰,也存在可见性问题。解决的方法是对 getValue() 方法也进行同步。

可以使用如下方法实现原子性:

  • synchronized
  • Lock

synchronized 和 Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将变量的修改刷新到主存当中。因此可以保证原子性与可见性。缺点是开销比较大。

除此之外,从 JDK1.5 开始,Java 提供了 java.util.concurrent.atomic 包,这个包中的原子操作类提供了线程安全地更新一个变量的方式。以 AtomicInteger 为例展示一下用法。

AtomicInteger 的常用方法:

  • public final int getAndSet(int newValue):以原子方式设置为新值并返回旧值
  • public final int addAndGet(int delta):以原子方式与delta相加并返回结果
  • public final int getAndIncrement():以原子方式将当前值加1并返回自增前的值
  • public final int getAndDecrement():以原子方式将当前值减1并返回自增前的值

示例如下:

public class AtomicIntegerDemo {

    private static AtomicInteger ai = new AtomicInteger(10);
    
	public static void main(String[] args) {
        System.out.println(ai.getAndIncrement());
    	System.out.println(ai.addAndGet(5));
   	    System.out.println(ai.getAndSet(8));
    }
}

输出结果:

10
16
16

可见性

当一个线程对一变量修改后,还没有来得及将修改后的值写回到主存,此时另一线程对此变量进行访问,它并不知道变量已经修改过,使用的仍旧是修改之前的值,即一个线程对共享变量的修改对其它线程是不可见的。

可见性是指一个线程对共享变量做了修改之后,其它线程立即能够看到(感知到)该变量的变化。

在 Java 中,可以通过 volatile、synchronized、Lock、final 实现可见性。

  • volatile:volatile变量值修改后立刻同步到主内存,每次使用 volatile 变量前会从主内存中读取最新值,保证了多线程之间的操作变量的可见性
  • synchronized:在同步方法/同步块开始时(Monitor Enter),使用共享变量时会从主内存中读取最新值到线程私有的工作内存中,在同步方法/同步块结束时(Monitor Exit),会将线程私有的工作内存中的值写入到主内存进行同步
  • Lock:常用 ReentrantLock(重入锁) 来实现可见性,与 synchronized 有相同的语义
  • final:final 修饰的变量一旦初始化完成,其它线程就可以看到

猜你喜欢

转载自blog.csdn.net/weixin_43320847/article/details/83240008