volatile关键字可以算得上是JVM提供的最轻量级的同步机制,但是它比较难以被完整正确的理解,我在之前也看了一些volatile的文章,但是看完之后都感觉在云里雾里。以至于很多人都不习惯去用它,遇到多线程竞争资源的场景一律使用synchronized关键字进行同步。了解volatile变量的语义对了解多线程操作的其它特性很有意义,下面我们就来看一下volatile的语义是什么?
首先,不用那么通俗易懂的语言介绍一下这个关键字的作用。
当一个变量定义为volatile之后,它将具备两种特性。
第一,保证此变量对所有线程的可见性,这里的“可见性”是指当一个线程修改了这个变量的值,新的值对于其它线程来说是立即得知的。而普通的变量不能做到这一点,普通变量的值在线程间传递均需要通过共享内存。例如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一个线程B在线程A回写完成了之后再从共享内存进行读取操作,新变量才会对线程B可见。
关于volatile变量的可见性,经常会被开发人员误解,认为以下描述成立:“volatile变量对所有线程是立即可见的,对volatile变量所有的写操作都能立即返回到其它线程中,换句话说,volatile变量在各线程中都是一致的,所以基于volatile变量的运算在并发下是安全的”。因此,volatile很容易被误用,用来进行原子性操作。这句话前面并没有错,但是不能得到“volatile修饰的变量在并发下一定是安全的”这个结论。
volatile变量在各个线程的工作栈中不存在一致性问题(在各个线程的工作栈中,volatile变量也可以存在不一致的情况,但是由于每次使用之前都要刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题),但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的,我们可以看一下下面这段代码:
1 | public class VolatileTest { |
这段代码发起了20个线程,每个线程对race变量进行10000次自增操作。如果这段代码能够正确并发的话,最后输出结果应该是200000。但是我运行了几次,结果分别是72597、83889、68223,都是一个小于200000的结果。
这是为什么呢?问题就在自增运算race++;
上,我们用javap反编译这段代码后,发现只有一行的increase()方法在Class文件中是由4条字节码指令构成(并不是一个原子操作,不可拆分的操作)。从字节码层面上很容易就能分析出并发失败的原因了:当getstatic指令把race的值取到操作栈顶时(用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值),volatile关键字保证了race的值在此时是正确的,但是在执行iconst_1、iadd的时候,其它线程可能已经把race的值增大了,而在操作栈顶的值就变成了过期的数据,所以putstatic执行后就可能把较小的race值同步到共享内存之中。
race++;
的Class字节码:
1 | getstatic |
客观来讲,这里用字节码来分析并发问题,仍然是不严谨的,因为一条字节码指令也不一定是一个原子操作。
由于volatile变量只能保证可见性,不能保证原子性。在不符合一下两条规则的运算场景中,我们仍然要通过加锁(比如使用synchronized关键字)来保证原子性。
- 运算结果并不依赖变量的当前值,或者能够保证只有单一的线程修改变量的值。
- 变量不需要与其它的状态变量共同参与不变约束。
像如下的代码的这类场景就很适合使用volatile变量来控制并发,当shutdown()被调用的时候,能保证所有线程中执行的doWork()方法都停下来。
1 | volatile boolean isShutdown; |