volatile关键字的原理及其应用
在某些需要避免并发问题的场景中,我们总能见到volatile的身影,例如在双重检查锁机制下获取单实例对象,第二次检查的意义就在于能其他之前没有获取到锁的线程此时内部的instance已经不是null了:
又比如在Java中的那些基于COW(写时复制)机制的并发类中,基于读写分离的思想,写时加锁,读时无锁,写入完成后其他线程内拿到的值就能立刻同步。例如CopyOnWriteArrayList:
不能简单认为使用 volatile就能避免并发的问题,对于volatile的原理需要了解,才能应用到正确的使用场景中去。
我们首先要知道,volatile能做什么,通常来讲,我们一般使用volatile来进行线程间的变量同步,我们都知道,Java中并发的线程都有自己的工作内存,各线程的工作内存是互不可见的,当多个线程要访问某个对象的时候,就会从共享的驻内存中复制一份到自己的工作内存中,后续对该对象,或者说资源的访问和读写,都各自在自己的工作内存中进行,最后写回到主内存中,如果线程在运行过程中,已经将该资源复制到工作内存中了,但是没有立刻使用,即使在后面再使用的情况下(只是举例子,不考虑编译器优化),也不会再去主内存中去获取最新内容,而被volatile关键字修饰的资源,工作线程在每次访问的时候都会从主内存中刷新该资源,这就是我们通常所说的volatile保证内存可见性的具体体现,那么volatile是怎么做到这一点的呢?如下图(引用自周志明深入理解JVM第三版,我没安装翻译成汇编语言的插件):
可以看到编译成的汇编语言代码中对使用了volatile修饰的变量,在进行操作后有一个add1 $0x0,(%esp)的操作,这个语意是将寄存器的值+0,这里的关键 在于lock前缀,查询IA32手册可知,它的作用是将本处理器的缓存写入了内存,该写入动作也会引起 别的处理器或者别的内核无效化(Invalidate)其缓存,这种操作相当于对缓存中的变量“store和write”操作。所以通过这样一个空操作,让其他线程中缓存的值失效,在使用时就必须重新回到主存中获取该最新值,可让前面volatile变量的修改对其他处理器立即可见。
另外这句指令也相当于是一个内存屏障告诉编译器不能把后边的操作放到内存屏障之前,指令重排序会带来什么问题呢?可以参照这个代码示例: