[JAVA] Java 并发之 volatile 的实现原理


在多线程并发编程中,volatile 保证了共享变量的可见性。可见性的意思是当一个线程修改了一个共享变量时,另一个线程能及时读到这个修改的值。当然 synchronized 也能达到相同的效果,但在恰当的使用 volatile 修饰变量的情况,是比 synchronized 效率更高的,因为它不会引起线程上下文的切换和调度。

volatile 的定义

Java 语言规范 8.3.1.4 一节中对 volatile 的定义如下:

Java 编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java 语言提供了 volatile,在某些情况下比锁要更加方便。如果一个字段被声明成 volatile,Java 线程内存模型确保所有线程看到这个变量的值时一致的。

volatile 的实现原理

volatile 具体是怎么保证可见性的,先通过一个例子来分析。这段简单的代码是修改 volatile 修饰的变量然后生成汇编代码(编译处理器为 Intel Core i7,x86-64 指令集):

Java 代码:

public class Main {
    // 普通变量
    long sum = 0;
    // volatile 修饰的变量
    volatile long volSum = 0;

    public Main() {
        for (int i = 0; i < 100000; i++) { // 为了得到汇编代码循环100000次 =。=
            sum += i;
            volSum += i;
        }
    }

    public static void main(String[] args) {
        new Main();
    }
}

生成的汇编代码:

# {method} {0x0000000129168270} '<init>' '()V' in 'org/demo/Main'
#           [sp+0x60]  (sp of caller)
...
0x00000001111218a7: add %rax,%rbx
0x00000001111218aa: mov %rbx,0x10(%rsi)  ;*putfield sum
                                         ; - org.demo.Main::<init>@30 (line 11)
...
0x00000001111218b8: add %rax,%rbx
0x00000001111218bb: mov %rbx,0x40(%rsp)
0x00000001111218c0: vmovsd 0x40(%rsp),%xmm0
0x00000001111218c6: vmovsd %xmm0,0x18(%rsi)
0x00000001111218cb: lock addl $0x0,(%rsp)  ;*putfield volSum
                                           ; - org.demo.Main::<init>@41 (line 12)
...

通过对比可以发现,有 volatile 修饰的变量进行写操作的时候会多出 lock addl 前缀指令的汇编代码。查询 IA32 手册可知,addl 指令是一个空操作,而 lock 的作用是将本处理器的缓存写回内存,相当于一个内存屏障 (Memory Barrier)

由于处理器和内存之间的运算速度有几个数量级的差距,为了提高处理速度,处理器不直接和内存进行通行,而是先将内存的数据读取到内部高速缓存(L1、L2或其他)后再进行操作。在多处理器下,每个处理器都有自己的高速缓存,然后又共享一个主内存,所以当多个处理器的操作涉及到了主内存的同一块区域,就可能导致各自的缓存数据不一致(过期)。为了解决这个缓存一致性 (Cache Coherence) 问题,就要求各个处理器根据一些协议(如 MESI、MOSI、MSI、Firefly 等)来进行读写:每个处理器通过嗅探在总线上传播的数据来检查自己的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态;当处理器对这个数据进行修改操作时,就会重新从主内存中把数据读取到处理器缓存中。

所以对 volatile 修饰的变量进行写操作,JVM 就会向处理器发送一条 lock 前缀指令,将这个变量所在缓存行的数据写回到系统内存,然后通过缓存一致性协议,保证多核处理器下的各核数据一致。

不过值得注意的一点是, volatile 只是保证了变量的可见性,并不保证操作的原子性。在一些操作下仍需要通过加锁来保证原子性,比如单例模式一种常见的线程安全实现方式,双重验证 (Double Check Lock):

public class Singleton {
    private volatile static Singleton INSTANCE;

    private Singleton() { }

    public static Singleton getInstance() {
        if(null != INSTANCE){
            synchronized(Singleton.class) {
                if(null != INSTANCE) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }

    // Singleton.getInstance();
}