java-doc-volatile

volatile

修饰符volatile告诉编译器,由volatile修饰的变量可以被程序的其他部分随意修改。在多线程程序中,有时两个或多个线程共享相同的变量,出于效率方面的考虑,每个线程自身可以保存这种共享变量的私有副本,真正的(或主)变量副本在各个时间被更新,例如当进入同步方法时。虽然这种方式可以工作,但是有时效率不高。在有些情况下,重要的是变量的主副本总是反映自身的当前状态。为了确保这一点,简单地将变量修改为volatile,这回告诉编译器必须总是使用volatile变量的主副本(或者,至少总是保持所有私有版本和最新的主副本一直,反之亦然)。此外,访问主变量的顺序必须和所有私有副本相同,以精确地顺序执行。

概括说法:

  • 可见性:让一个线程对共享变量的修改,能够及时的被其他线程看到
  • 禁止缓存:volatile变量的访问控制符会加个ACC_VOLATILE,不会被CPU告诉缓存区缓存
  • 关闭优化:CPU指令重排不会对volatile修饰的变量进行代码优化

问题分析1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class LearnVolatile1 {
int i = 0;
boolean isRunning = true;

public static void main(String args[]) throws InterruptedException {
LearnVolatile1 test = new LearnVolatile1();

new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread running start...");
while(test.isRunning){
test.i++;
}

System.out.println("thread running end : " + test.i);
}
}).start();

Thread.sleep(3000L);
test.isRunning = false;
System.out.println("shutdown...");
}
}

对于上述代码,无论我们程序运行多少遍,我们都无法把i的值打印出来,这原因并不是Java的Bug。

我们的代码中,由于isRunning没有被volatile修饰,因此是非可见的,所以CPU指令重排的时候会对关于这变量的使用代码进行优化,大致如下

1
2
3
4
5
6
7
8
9
public void run() {
System.out.println("thread running start...");
if(test.isRunning){
while(true){
test.i++;
}
}
System.out.println("thread running end : " + test.i);
}

通过代码可以看到,while的判断条件变动了,所以我们在3秒后把isRunning改成false,循环也没有退出。

要预防这种因为多个线程公用一个变量因为可见性导致代码被CPU指令重排导致的问题,只需要给变量加个volatile修饰即可。