volatile
volatile的作用
- 防止指令重排序
- 实现变量的可见性
- 保证单次的读/写具有原子性
volatile保证可见性的demo:
public class VolatileDemo {
int a = 1;
int b = 2;
public void change() {
a = 3;
b = a;
}
public void print() {
System.out.println("a : " + a + ", b : " + b);
}
public static void main(String[] args) {
while (true) {
final VolatileDemo volatileDemo = new VolatileDemo();
new Thread(() -> {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
volatileDemo.change();
}).start();
new Thread(() -> {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
volatileDemo.print();
}).start();
}
}
}
正常来说只会有两种结果:
a : 1, b : 2 表示执行完print()还没执行change()
a : 3, b : 3 表示执行完change()马上执行print
第三种:
a : 1, b : 3 表示change()执行到一半的时候,print()开始执行,因此出现第一个线程对变量a的修改,第二线程还无法感知到。a :3,b : 2 同理。
volatile与原子性
volatile能保证原子性吗?i++为什么不能保证原子性?
保证单词的读/写操作具有原子性。
i++实际有三步操作,先读取i的值,+1,再设置回对应的对象值。如果多个线程在这三步之间同时操作,就有可能出问题。
volatile不能保证原子性demo:
public class VolatileVisibilityDemo {
volatile int i;
public void addI(){
i++;
}
public static void main(String[] args) throws InterruptedException {
final VolatileVisibilityDemo volatileVisibilityDemo = new VolatileVisibilityDemo();
for (int n = 0; n < 1000; n++) {
new Thread(() -> {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
volatileVisibilityDemo.addI();
}).start();
}
Thread.sleep(10000); // 等待10秒,保证上面程序执行完成
System.out.println(volatileVisibilityDemo.i);
}
}
结果:
971

以JMM的这个示意图为例,如果线程A、B同时拿到了主内存中的变量i,此时值为0,两个线程在各自的内存空间中对i的值+1以后,如果A线程先写回到主内存中i的空间,此时B线程已经读取到了0的值,也+1,也把1写回到主内存i中的地址,这时候最终的结果就成了1,所以上述demo的结果不为1000
volatile与long、double
之前32位机器上共享的long和double变量为什么要用volatile?现在也要吗?
32位机器上,会把long、double设置为两个32位长度,高低32位的操作可能是不一致的,因此不能保证原子性。但是目前各种平台上的上用虚拟机选择把64位的读写操作作为原子操作独代,所以一般不用专门将long、double声明为volatile
volatile与可见性
volatile是如何实现可见性的? 内存屏障。
内存屏障又称内存栅栏,是一个 CPU 指令。
在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止 + 特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序。
指令重排的例子:
public void mySort() {
int x = 1; // 1
int y = 1; // 2
x = x + 1; // 3
y = x * x; // 4
}
上述4条指令中,可能的执行顺序为1 2 3 4,但是因为1 2之间没有数据依赖关系,所以可能的结果为2 1 3 4以及1 3 2 4,但是不可能会出现3 1 2 4,因为3 1之间存在这数据依赖的关系。
volatile禁止指令重排的demo:
public class ResortDemo {
int a = 0;
boolean flag = false;
public void method1() {
a = 1; // 1
flag = true; // 2
}
public void method2() {
if (flag) { // 3
a = a + 5; // 4
System.out.println("a value is : " + a); // 5
}
}
public static void main(String[] args) {
ResortDemo resortDemo = new ResortDemo();
new Thread(resortDemo::method1).start();
new Thread(resortDemo::method2).start();
}
}
如果按正常的顺序执行,1 2 3(true) 4 5,即
a = 1; // 1
flag = true; // 2
a = a + 5; // 4
System.out.println("a value is : " + a); // 5
最终得到的结果是a value is : 6
但是a和flag之间没有数据依赖关系,就有可能出现的执行顺序是2 3(true) 4 5 1,即
flag = true; // 2
a = a + 5; // 4
System.out.println("a value is : " + a); // 5
a = 1; // 1
这个时候结果会是a value is : 5
volatile与有序性
volatile是如何实现有序性的? happens-before原则
happens-before规则中有一条是volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个 volatile 域的读。上述demo添加了volatile以后,相当于在2中对volatile变量flag的写入、2中flag的读取添加了内存屏障,保证了指令不会被重排序:
public class ResortDemo {
int a = 0;
volatile boolean flag = false;
public void method1() {
a = 1; // 1
flag = true; // 2
}
public void method2() {
if (flag) { // 3
a = a + 5; // 4
System.out.println("a value is : " + a); // 5
}
}
public static void main(String[] args) {
ResortDemo resortDemo = new ResortDemo();
new Thread(resortDemo::method1).start();
new Thread(resortDemo::method2).start();
}
}
根据happens-before原则,有三个happens-before关系:
- 根据程序次序关系,1
happens-before2 且 3happens-before4 - 根据
volatile规则,2happens-before3 - 根据
happens-before传递性规则,1happens-before4

因为以上规则,当线程 A 将 volatile 变量 flag 更改为 true 后,线程 B 能够迅速感知。
volatile保证有序性的另一个demo:
public class Singleton {
private static Singleton instance = null; // 1
// 私有化构造器
private Singleton() {} // 2
public static Singleton getInstance() { // 3
if (instance == null) { // 4 第一次检查
synchronized(Singleton.class) { // 5 加锁
if (instance == null) { // 6 第二次检查
instance = new Singleton(); // 7 实例化
} // 8
} // 9
} // 10
return instance; // 11
} // 12
} // 13
上述是单例模式的双重检查代码,其中,语句7可以进一步分解为以下步骤:
memery = allocate(); // 7.1 分配内存
instance(memory); // 7.2 初始化内存
instance = memory; // 7.3 将内存地址指向给对象
双重检查的代码的问题在于当存在两个线程同时访问该段代码时,假设线程A执行到了7,并进行了指令重排序,排序的结果如下:
memery = allocate(); // A 7.1 分配内存
instance = memory; // A 7.3 将内存地址指向给对象
instance(memory); // A 7.2 初始化内存
对单个线程而言,这样的指令重排不影响最终的结果,当A执行到上述的步骤2,即A 7.3时,instance该对象已经是非空,已经分配到了内存。此时线程B执行到了4,检查时发现instance == null不满足,直接退出返回这个分配到了内存,但是未初始化的对象,导致后续出错。
解决方案之一是直接在instance加上volatile,禁止指令重排,保证7.1->7.2->7.3的顺序执行。
public class Singleton {
private volatile static Singleton instance = null; // 1
// 私有化构造器
private Singleton() {} // 2
public static Singleton getInstance() { // 3
if (instance == null) { // 4 第一次检查
synchronized(Singleton.class) { // 5 加锁
if (instance == null) { // 6 第二次检查
instance = new Singleton(); // 7 实例化
} // 8
} // 9
} // 10
return instance; // 11
} // 12
} // 13
JMM会针对编译器制定 volatile 重排序规则表。

NO表示禁止重排序。
为了实现 volatile 内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM 采取了保守的策略。
- 在每个 volatile 写操作的前面插入一个
StoreStore屏障。 - 在每个 volatile 写操作的后面插入一个
StoreLoad屏障。 - 在每个 volatile 读操作的后面插入一个
LoadLoad屏障。 - 在每个 volatile 读操作的后面插入一个
LoadStore屏障。
volatile 写是在前面和后面分别插入内存屏障,而 volatile 读操作是在后面插入两个内存屏障。
| 内存屏障 | 说明 |
|---|---|
StoreStore屏障 | 禁止上面的普通写和下面的 volatile 写重排序。 |
StoreLoad 屏障 | 防止上面的 volatile 写与下面可能有的 volatile 读/写重排序。 |
LoadLoad 屏障 | 禁止下面所有的普通读操作和上面的 volatile 读重排序。 |
LoadStore 屏障 | 禁止下面所有的普通写操作和上面的 volatile 读重排序。 |


说下volatile的应用场景?
使用 volatile 必须具备的条件
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
- 只有在状态真正独立于程序内其他内容时才能使用 volatile。
参考文章:
volatile-的实现原理
本文详细解析volatile关键字的作用,包括防止指令重排序、保证变量可见性和单次读写原子性。通过示例说明其与原子性、long/double处理、内存屏障及应用场景的关联。

3569

被折叠的 条评论
为什么被折叠?



