官方文档中对volatile
关键字的说明是这样的:
The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes. A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable. Using volatile variables reduces the risk of memory consistency errors, because any write to a volatile variable establishes a happens-before relationship with subsequent reads of that same variable. This means that changes to a volatile variable are always visible to other threads. What’s more, it also means that when a thread reads a volatile variable, it sees not just the latest change to the volatile, but also the side effects of the code that led up the change.
简单来讲,就是使用volatile
只能保证可见性,不能保证原子性。
假如让一个volatile
的integer
自增(i++),其实要分成3步:
- 读取
volatile
变量值到local; - 增加变量的值;
- 把local的值写回,让其它的线程可见。
这3步的JVM指令为:
mov 0xc(%r10),%r8d ; Load
inc %r8d ; Increment
mov %r8d,0xc(%r10) ; Store
lock addl $0x0,(%rsp) ; StoreLoad Barrier
最后一条是内存屏障。内存屏障(memory barrier)是一个CPU指令。基本上,它的作用有如下两点:
- 确保一些特定操作执行的顺序;
- 影响一些数据的可见性(可能是某些指令执行后的结果)。
编译器和CPU可以在保证输出结果一样的情况下对指令重排序以优化性能(这种优化在大多数程序运行的场景是没问题的,少数并发的情况下,就需要开发者手动禁用以获得正确的结果)。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个CPU核心执行的。
内存屏障(memory barrier)和volatile
有什么关系?上面的虚拟机指令里面有提到,如果你的字段是volatile
,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。这意味着如果对一个volatile
字段进行写操作,那么:
-
一旦完成写入,任何访问这个字段的线程将会得到最新的值。
-
在写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到主存。
明白了内存屏障这个CPU指令,回到前面的JVM指令:从Load
到Store
到内存屏障,一共4步,其中最后一步JVM让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但**中间的几步(从Load
到Store
)**是不安全的,中间如果其他的CPU修改了的值将会丢失。
所以不要将volatile
用在getAndOperate
场合(这种场合不原子,需要再加锁),仅仅set
或者get
的场景是适合volatile
的。
当面对getAndOperate
的场合时,就需要用到原子类(AtomicXXX
)了,拿AtomicInteger
来举例,其源码如下:
// AtomicInteger.class
public class AtomicInteger extends Number implements Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
private volatile int value;
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception var1) {
throw new Error(var1);
}
}
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
// Unsafe.class
public final class Unsafe {
public final native boolean compareAndSwapInt(Object obj, long offset, int oldValue, int newValue);
public final int getAndAddInt(Object obj, long offset, int v) {
int oldValue;
do {
oldValue = this.getIntVolatile(obj, offset);
} while(!this.compareAndSwapInt(obj, offset, oldValue, oldValue + v));
return oldValue;
}
}
其中Unsafe
类中的compareAndSwapInt
即为经典的CAS算法,对应的源码:
// unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
从源码可以看出AtomicInteger
也是使用volatile
来保证可见性的,当要给原子类设置新值时,会先取出内存中的旧值,然后将旧值,新值一同传人native的compareAndSwapInt
方法中,该方法调用的Atomic::cmpxchg(x, addr, e)
实际上对应CPU的cmpxchg
指令,该指令的作用是:把旧值与内存中的值相比较,如果相同表明此刻没有其他线程做了修改,则把新值写入内存并返回旧值,否则表明内存值已被其他线程修改,那么直接返回内存中的值。
综合起来看整个过程就明了了,当想要修改原子类的值时,会先比较当前取到的旧值是否跟内存一致,若一致则将新的值写入内存,否则进行循环,重新取出内存中的值再次进行尝试。cmpxchg
指令保证了比较和写值操作的原子性,所以整个AtomicInteger
是既能够保证可见性又能够保证原子性的。