volatile怎么保证的可见性
可见性案例
public class ApiTest {
public static void main(String[] args) {
final VT vt = new VT();
Thread Thread01 = new Thread(vt);
Thread Thread02 = new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException ignore) {
}
vt.sign = true;
System.out.println("vt.sign = true 通知 while (!sign) 结束!");
}
});
Thread01.start();
Thread02.start();
}
}
class VT implements Runnable {
public boolean sign = false;
public void run() {
while (!sign) {
}
System.out.println("可见");
}
}
这段代码最后的结果是不会输出"可见" 会一直死循环
加上volatile关键字
class VT implements Runnable {
public volatile boolean sign = false;
public void run() {
while (!sign) {
}
System.out.println("可见");
}
}
测试结果
vt.sign = true 通知 while (!sign) 结束!
可见
volatile关键字是java虚拟机提供的最轻量级同步机制,用来修饰全局变量,在对sign变量修饰后,""可见就可以输出了
有无volatile时内存变化图
- 无volatile时,内存的变化
- 有volatile时候,内存变化
当 sign 没有 volatitle 修饰时,线程1对变量进行操作,线程2并不会拿到变化的值,内存也不会刷新变量的值,当我们把变量使用 volatile 修饰时线程1对变量进行操作时,会把变量变化的值强制刷新的到主内存。当线程2获取值时,会把自己的内存里的 sign 值过期掉,之后从主内存中读取。所以添加关键字后程序如预期输出结果。
查看JVM命令
public volatile boolean sign;
descriptor: Z
flags: ACC_PUBLIC, ACC_VOLATILE
org.itstack.interview.test.VT();
descriptor: ()V
flags:
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field sign:Z
9: return
LineNumberTable:
line 35: 0
line 37: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lorg/itstack/interview/test/VT;
public void run();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field sign:Z
4: ifne 10
7: goto 0
10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
13: ldc #4 // String 可见
15: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
18: return
LineNumberTable:
line 40: 0
line 42: 10
line 43: 18
LocalVariableTable:
Start Length Slot Name Signature
0 19 0 this Lorg/itstack/interview/test/VT;
StackMapTable: number_of_entries = 2
frame_type = 0 /* same */
frame_type = 9 /* same */
}
可以发现在指令码的第三行多了个ACC_VOLATILE,这样是看不出来为什么保证了内存的可见性的
查看汇编指令
0x0000000003324cda: mov 0x74(%r8),%edx ;*getstatic state
; - VT::run@28 (line 27)
0x0000000003324cde: inc %edx
0x0000000003324ce0: mov %edx,0x74(%r8)
0x0000000003324ce4: lock addl $0x0,(%rsp) ;*putstatic state
; - VT::run@33 (line 27)
可以发现有volatile关键字和没有volatile关键字,主要差别在于多了一个 lock addl $0x0,(%rsp),也就是lock的前缀指令。
lock指令相当于一个内存屏障,它保证如下三点:
- 将本处理器的缓存写入内存。
- 重排序时不能把后面的指令重排序到内存屏障之前的位置。
- 如果是写入动作会导致其他处理器中对应的内存无效。
那么,这里的1、3就是用来保证被修饰的变量,保证内存可见性。
不加volatile也可见吗?
class VT implements Runnable {
public boolean sign = false;
public void run() {
while (!sign) {
System.out.println("嘿嘿");
}
System.out.println("呵呵");
}
}
修改后去掉了volatile 关键字,现在的运行结果是:
嘿嘿
嘿嘿
嘿嘿
嘿嘿
vt.sign = true 通知 while (!sign) 结束!
呵呵
Process finished with exit code 0
发现结果是又可见了,这是因为在没 volatile 修饰时,jvm也会尽量保证可见性。有 volatile 修饰的时候,一定保证可见性。
总结
- volatile会控制被修饰的变量在内存操作上主动把值刷新到主内存,JMM 会把该线程对应的CPU内存设置过期,从主内存中读取最新值。
- volatile 防止指令重排是通过内存屏障,volatile 的内存屏故障是在读写操作的前后各添加一个 StoreStore屏障,也就是四个位置,来保证重排序时不能把内存屏障后面的指令重排序到内存屏障之前的位置。
- 另外 volatile 并不能解决原子性,如果需要解决原子性问题,需要使用 synchronzied 或者 lock。
,JMM 会把该线程对应的CPU内存设置过期,从主内存中读取最新值。
- volatile 防止指令重排是通过内存屏障,volatile 的内存屏故障是在读写操作的前后各添加一个 StoreStore屏障,也就是四个位置,来保证重排序时不能把内存屏障后面的指令重排序到内存屏障之前的位置。
- 另外 volatile 并不能解决原子性,如果需要解决原子性问题,需要使用 synchronzied 或者 lock。