正确使用volatile

volatile语义

     1. 内存可见性
        一个线程修改了volatile修饰的变量的值后,新值对另一个线程立即可见
        
     2. 禁止指令重排序优化
        对volatile修饰对变量进行读写操作的前后插入内存屏障以达到禁止volatile修饰的变量和其前后其他变量的指令重排序
        
     3. 使用时的表现:
        对volatile修饰的变量进行读操作时,将线程工作内存里的变量副本置为无效,然后从主内存读取并刷新工作内存的变量副本
        对volatile修饰对变量进行写操作时,将线程工作内存里对变量副本刷新到主内存
        
     4. 注意事项:
        volatile只能保证可见性和对此变量单独操作的原子性,当对volatile修饰的变量涉及到复合操作时不能保证原子性,也就不能保证并发的安全性了
        比如:
        private static volatile int count = 0;
                  count++; //累加操作是一个复合操作,即使volatile修饰,多线程时也不能保证原子性
       
       private static volatile boolean flag = false;
              flag = true; //当作标志位使用时,使用volatile可以保证多线程并发安全性

参考文章【编程玄学】一个困扰我122天的技术问题,我好像知道答案了,不使用volatile时由于JIT优化,会出现很多表面看起来莫名其妙的问题,所以一定要正确使用volatile,不要依赖于编译器和JVM的优化操作。

举例说明

环境:

1
2
3
4
[qiaojian@Mac ~ ]% java -version                                                          [0]
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

如下代码片段,注意两个地方:

  1. 设置flag为true之前sleep 2毫秒
  2. 在while循环里累加计数
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
public class TestVolatile {
private static int count = 0;
public static void main(String[] args) {
NonVolatile nonVolatile = new NonVolatile();
nonVolatile.start();
while (!nonVolatile.flag) {
count++;
}
System.out.println("FLAG:TRUE 主程序退出" + count);
}

static class NonVolatile extends Thread {
//private volatile boolean flag = false;
private boolean flag = false;
@Override
public void run() {
try {
// 程序执行结果还依赖于sleep时间
Thread.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
flag = true;
System.out.println("SET FLAG=TRUE");
}
}
}

编译运行代码执行结果如下:

1
2
3
[qiaojian@Mac ~ ]% java TestVolatile                                                      [0]
SET FLAG=TRUE
FLAG:TRUE 主程序退出 61475

然后我们把sleep时间设置长一点比如10毫秒

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
public class TestVolatile {
private static int count = 0;
public static void main(String[] args) {
NonVolatile nonVolatile = new NonVolatile();
nonVolatile.start();
while (!nonVolatile.flag) {
count++;
}
System.out.println("FLAG:TRUE 主程序退出" + count);
}

static class NonVolatile extends Thread {
//private volatile boolean flag = false;
private boolean flag = false;
@Override
public void run() {
try {
// 程序执行结果还依赖于sleep时间
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
flag = true;
System.out.println("SET FLAG=TRUE");
}
}
}

再次编译运行看看效果,发现此时主线程死循环了未退出程序

1
2
3
[qiaojian@Mac ~ ]% java TestVolatile                                                      [0]
SET FLAG=TRUE
^C%

再次执行,这次加上禁止JIT优化选项,发现主线程可以正常退出了

1
2
3
[qiaojian@Mac ~ ]% java -Xint TestVolatile                                                [0]
SET FLAG=TRUE
FLAG:TRUE 主程序退出 488111

由此可见,sleep时间的长短对共享变量的可见性也有影响,为什么呢?

因为sleep时间短,while循环短不会触发JIT优化操作。while循环过长超过一定次数时会触发JIT优化操作,将

1
2
3
while (!nonVolatile.flag) {
count++;
}

优化为:

1
2
3
4
5
if (!nonVolatile.flag) {
while (true) {
count++;
}
}

然后我们将flag变量使用volatile修饰,会发现不管设置flag为true之前sleep多久,主线程都可以正常退出。这就是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
public class TestVolatile {
private static int count = 0;
public static void main(String[] args) {
NonVolatile nonVolatile = new NonVolatile();
nonVolatile.start();
while (!nonVolatile.flag) {
count++;
}
System.out.println("FLAG:TRUE 主程序退出" + count);
}

static class NonVolatile extends Thread {
private volatile boolean flag = false;
//private boolean flag = false;
@Override
public void run() {
try {
// 程序执行结果还依赖于sleep时间
Thread.sleep(30*1000);
} catch (Exception e) {
e.printStackTrace();
}
flag = true;
System.out.println("SET FLAG=TRUE");
}
}
}
1
2
3
[qiaojian@Mac ~ ]% java TestVolatile                                                      [0]
SET FLAG=TRUE
FLAG:TRUE 主程序退出 1628189977
-------------本文结束感谢您的阅读-------------
Good for you!