官方文档中对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只能保证可见性,不能保证原子性。

假如让一个volatileinteger自增(i++),其实要分成3步:

  1. 读取volatile变量值到local;
  2. 增加变量的值;
  3. 把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指令:从LoadStore到内存屏障,一共4步,其中最后一步JVM让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但**中间的几步(从LoadStore)**是不安全的,中间如果其他的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是既能够保证可见性又能够保证原子性的。