文章目录
一、前言
volatile,这个关键字,是JDK提供给我们使用的,之前也总结过一篇文章不过有点浅显,只是说明一下它的特性,这篇文章打算详细地总结下相关的概念与底层细节。
之前的总结:JUC(8)Java内存模型-JMM和Volatile关键字
volatile的特性:
- 1、保证可见性 (这就要涉及JMM)
- 2、不保证原子性
- 3、禁止指令重排
二、CPU的乱序执行
CPU在执行代码时是乱序的,这里说的乱序是指代码编译后的指令的执行顺序,也就是说指令是乱序执行的。
为什么要打乱顺序呢?答:是为了提高CPU的执行效率。
这一块,具体的介绍可以看之前的读书笔记:为什么要进行指令重排呢?
在此,也可以给出一个代码示例来验证下:
执行下面代码,如果CPU没有乱序执行的话,那么 a = 1
必然在 x = b
前面,b = 1
必然在 y = a
的前面
我们可以得到什么结论,也就是 x
和 y
肯定不能同时为 0
。
但是实际情况是,我们会遇到 x
和 y
同时为 0
的情况。(可能要循环几百万次才能遇到一次)
x = 0;
y = 0;
a = 0;
b = 0;
Thread one = new Thread(new Runnable() {
public void run() {
//由于线程one先启动,下面这句话让它等一等线程two. 可根据自己电脑的实际性能适当调整等待时间.
//sleep(100000);
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
}
});
one.start();
two.start();
one.join();
two.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.err.println(result);
break;
}
三、乱序可能会造成的问题
先说答案,会引起数据错误(数据安全问题)。
用个例子来验证吧,背景:DCL单例模式加了volatile关键字。
class T{
int m = 8;
}
T t = new T();
反编译汇编码:
0 new #2 <T>
3 dup
4 invokespeecial # 3 <T.<init>>
7 astore_1
8 return
12345
对汇编码逐步分析:
new #2 <T>
:创建m = 0
的对象并且栈帧中有一个引用指向该对象(此时m=0)dup
:在我们的栈帧中复制一份引用(此时m=0)invokespecial #3 <T.<init>>
:弹出一个栈帧中的值,实例化它的构造方法(此时m=8)astore_1
:将我们栈帧的引用赋值给 t,这里1
指的是我们本地变量表中的第一位(此时m=8)
因为乱序的存在,当我们的 astore_1
在我们的 invokespeecial # 3 <T.<init>>
执行前执行,会导致我们的将我们没有实例化的对象赋值给 t
,所以m = 0
。此时如果去使用t,肯定会造成数据错误。
所以为了避免这种现象,我们要对 DCL
加 volatile
,那问题来了,我们 volatile
是怎么保证有序性的呢?
四、如何禁止指令重排序?
对于禁止指令重排序,从以下四个方面来谈:
- 代码层面
- 字节码层面
- JVM层面
- CPU层面
4.1 Java 代码层面
直接加一个 volatile 关键字即可