深入理解Java中的volatile关键字

volatile关键字可以算得上是JVM提供的最轻量级的同步机制,但是它比较难以被完整正确的理解,我在之前也看了一些volatile的文章,但是看完之后都感觉在云里雾里。以至于很多人都不习惯去用它,遇到多线程竞争资源的场景一律使用synchronized关键字进行同步。了解volatile变量的语义对了解多线程操作的其它特性很有意义,下面我们就来看一下volatile的语义是什么?

首先,不用那么通俗易懂的语言介绍一下这个关键字的作用。

当一个变量定义为volatile之后,它将具备两种特性。

第一,保证此变量对所有线程的可见性,这里的“可见性”是指当一个线程修改了这个变量的值,新的值对于其它线程来说是立即得知的。而普通的变量不能做到这一点,普通变量的值在线程间传递均需要通过共享内存。例如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一个线程B在线程A回写完成了之后再从共享内存进行读取操作,新变量才会对线程B可见。

关于volatile变量的可见性,经常会被开发人员误解,认为以下描述成立:“volatile变量对所有线程是立即可见的,对volatile变量所有的写操作都能立即返回到其它线程中,换句话说,volatile变量在各线程中都是一致的,所以基于volatile变量的运算在并发下是安全的”。因此,volatile很容易被误用,用来进行原子性操作这句话前面并没有错,但是不能得到“volatile修饰的变量在并发下一定是安全的”这个结论

volatile变量在各个线程的工作栈中不存在一致性问题(在各个线程的工作栈中,volatile变量也可以存在不一致的情况,但是由于每次使用之前都要刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题),但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的,我们可以看一下下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class VolatileTest {

public static volatile int race = 0;

public static void increase() {
race++;
}

private static final int THREADS_COUNT = 20;

public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
});
threads[i].start();
}

// 等待所有累加线程都结束
while (Thread.activeCount() > 1)
Thread.yield();

System.out.println(race);
}
}

这段代码发起了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
2
3
4
getstatic
iconst_1
iadd
putstatic

客观来讲,这里用字节码来分析并发问题,仍然是不严谨的,因为一条字节码指令也不一定是一个原子操作。

由于volatile变量只能保证可见性,不能保证原子性。在不符合一下两条规则的运算场景中,我们仍然要通过加锁(比如使用synchronized关键字)来保证原子性。

  • 运算结果并不依赖变量的当前值,或者能够保证只有单一的线程修改变量的值。
  • 变量不需要与其它的状态变量共同参与不变约束。

像如下的代码的这类场景就很适合使用volatile变量来控制并发,当shutdown()被调用的时候,能保证所有线程中执行的doWork()方法都停下来。

1
2
3
4
5
6
7
8
9
10
11
volatile boolean isShutdown;

public void shutdown() {
isShutdown = true;
}

public void doWork() {
while(!isShutdown){
// do stuff
}
}
坚持原创技术分享,您的支持将鼓励我继续创作!
显示 Gitment 评论