Java 并发编程梳理三:浅谈 volatile

文章目录
  1. 1. 简要概述
  2. 2. 场景应用
  3. 3. 深入探究
  4. 4. 参考文献
  5. 5. 更多内容

这世界上没有什么能阻止一个有正确态度的人完成他的目标,而一个态度不对头的人是无可救药的。

浅析完 synchronized,这次来谈谈“程度较轻的 synchronized”,即 volatile。相较于 synchronized,volatile 需要的代码少一些,运行时的开销也较少,但是,其仅能实现 synchronized 的一部分功能。

简要概述

volatile 是一种稍弱的同步机制,使用它来保证变量的更新操作通知到其他线程。当变量被声明为 volatile 类型后,编译器与运行时都会注意到这个共享的变量,从而不会重排序该变量上的操作与其他内存操作

  • volatile 变量不会被缓存在寄存器或其他处理器不可见的地方,故在读取 volatile 修饰的变量时,总是会返回最新写入的值
  • 访问 volatile 变量时,不执行加锁的操作,不会阻塞执行线程

使用 volatile 须满足两个条件

  • 对变量的写操作不依赖于当前值
  • 该变量未包含在具有其他变量的不变式中

第一个条件,意思即 volatile 变量不能用作线程安全计数器,即使形如 a++ 的增量操作好似一个单独操作。事实上,该增量操作是一个由读取 - 修改 - 写入操作序列构成的组合操作,得以原子方式执行,而 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
28
29
public class NumRange {

private volatile int up = 10;
private volatile int low = 0;

public int getUp() {
return up;
}

public void setUp(int upValue) {
if (upValue < low) {
throw new IllegalArgumentException();
}

this.up = upValue;
}

public int getLow() {
return low;
}

public void setLow(int lowValue) {
if (lowValue > up) {
throw new IllegalArgumentException();
}

this.low = lowValue;
}
}

上下界分别初始化为 10 和 0,此时,假设线程 t1 和 t2 在某一时刻同时执行了 setLow(9) 和 setUp(6),并通过了不变式的检查,但是范围 (9, 6) 无效。故只能通过 synchronized 确保两个 set 方法在每一时刻只执行一个线程。

场景应用

以下举例 volatile 关键字两个应用场景。

1.作为状态标记量

比如电商的高并发场景下,定义一个布尔类型的变量 isPromoted,控制代码是否执行促销逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Promotion {

private volatile boolean isPromoted;

public void run() {
if (isPromoted) {
// 促销
} else {
// 常规
}
}

public void setPromoted(boolean promoted) {
isPromoted = promoted;
}
}

用户的请求线程执行 run() 方法,若要开启促销模式,则通过后台设置发送一个请求,调用 setPromoted() 方法,设置 isPromoted 为 true。因为 isPromoted 经由 volatile 修饰,其一旦修改了,其他线程也可以拿到 isPromoted 的最新值。

2.双重检查

volatile 修饰的单例,确保代码的稳定性为 100%,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {

private volatile static Singleton sInstance;

public static Singleton getInstance() {
if (sInstance == null) {
synchronized (Singleton.class) {
if (sInstance == null) {
sInstance = new Singleton();
}
}
}

return sInstance;
}
}

深入探究

Java 虚拟机的内存模型中,分为主内存和工作内存,每一个线程对应一个工作内存,且共享主内存的数据。重点看操作普通变量和 volatile 变量的差异:

  • 普通变量,读操作会优先读取工作内存中的数据,若工作内存中不存在,则从主内存中拷贝一份数据到工作内存中;写操作只修改工作内存中的副本数据。这就是其他线程无法读取普通变量最新值的原因。
  • volatile 变量,读操作时,Java 内存模型 JMM 会把工作内存中对应的值设置为无效的,要求线程从主内存中读取数据;写操作时,JMM 会将工作内存中相应的数据刷新到主内存中。那么,其他线程就可以读取变量的最新值。

进一步,volatile 变量的内存可见性,是基于内存屏障 (Memory Barrier),即基于又称为内存栅栏的一个 CPU 指令实现的。

程序在运行时,为了提高性能,编译器和处理器会对指令重排序,JMM 为了保证在不同的编译器和 CPU 上都有着一样的效果,通过插入特定类型的内存屏障来禁止特定类型的编译器和处理器重排序。通俗点来说,即插入一条内存屏障告诉编译器和 CPU:任何指令都不能和这条内存屏障重排序。看下面例子:

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
public class Singleton {

private volatile static Singleton sInstance;

private static int i;
private static int j;
private static int k;

public static Singleton getInstance() {
if (sInstance == null) {
synchronized (Singleton.class) {
if (sInstance == null) {
i = 1; // step 1
j = 2; // step 2

sInstance = new Singleton(); // step 3

k = i + j; // step 4
}
}
}

return sInstance;
}
}

若变量 sInstance 未被 volatile 修饰,则语句 1、2、3 可以随意地进行重排序执行,意思即语句的执行过程可能是 1324 或 3214 等;若使用 volatile 修饰变量 sInstance,则语句 3 的前后各插入一个内存屏障。

反编译并比较 volatile 变量和普通变量生成的汇编代码,发现执行 volatile 变量会在之前多出一个 lock 前缀指令。其中,lock 指令相当于所说的内存屏障,确保:

  • 将当前 CPU 缓存行的数据写回到主内存
  • 写回内存的操作会导致其他 CPU 里缓存该内存地址的数据无效

至此,关于 Java 并发编程梳理三完毕。

本人才疏学浅,如有疏漏错误之处,望读者中有识之士不吝赐教,谢谢。

1
Email: [email protected] / WeChat: Wolverine623

您也可以关注我个人的微信公众号码农六哥第一时间获得博客的更新通知,或后台留言与我交流

参考文献

1.https://www.cnblogs.com/a31415926/p/6744485.html

更多内容

Java 并发编程梳理一:迈进大门

Java 并发编程梳理二:浅析 synchronized