易失性和原子性是两个不同的概念。易失性确保在不同线程中,某个预期的(内存)状态是真实的,而原子操作则确保对变量的操作是原子执行的。
以Java中的两个线程为例:
线程A:
value = 1
done = true
线程 B:
if (done)
System.out.println(value);
从 value = 0
和 done = false
开始,线程规则告诉我们,不确定 Thread B 是否会打印 value。此外,在这一点上,value 也是未定义的! 要解释这个问题,您需要了解一些关于Java内存管理的知识(可能很复杂),简而言之:线程可能创建变量的局部副本,并且JVM可以重新排列代码以优化它,因此不能保证以上代码按照确切的顺序运行。将 done 设置为 true,然后设置 value 为 1,可能是JIT优化的一种可能结果。
volatile
只能确保在访问这样一个变量的时候,新的值将立即对所有其他线程可见,并且执行顺序确保代码处于您期望它的状态。因此,在以上代码的情况下,将 done
定义为 volatile 将确保无论何时 Thread B 检查该变量,它要么为 false,要么为 true,如果为 true,则 value
也已被设置为 1。
作为 volatile 的副作用,这种变量的值被原子地设置为线程范围(以极小的执行速度代价)。但是,这仅在使用长(64位)变量(或类似变量)等 i.E. 32位系统上重要,在大多数其他情况下,设置/读取变量是原子的。但是,原子访问和原子操作之间有一个重要区别。volatile 只确保访问是原子的,而 Atomics 则确保 操作 是原子的。
接下来是一个例子:
i = i + 1;
无论你如何定义i,在执行上述代码行时,另一个线程读取值时可能会获取i或i + 1,因为该操作不是原子的。如果另一个线程将i设置为不同的值,则在最坏的情况下,线程A可能会将i重新设置为之前的值,因为它正处于基于旧值计算i + 1的中间阶段,然后再将i设置回那个旧值+1。说明:
Assume i = 0
Thread A reads i, calculates i+1, which is 1
Thread B sets i to 1000 and returns
Thread A now sets i to the result of the operation, which is i = 1
原子操作,如AtomicInteger,确保这些操作是原子性的。因此,上述问题不可能发生,一旦两个线程都完成,i 将成为 1000 或 1001。
volatile
关键字仅能进行单个原子读或写操作,而原子类型可以执行多种原子操作(例如getAndAdd) - Dmytro Melnychuk