深入理解volatile关键字
# 如何保证变量的可见性
# 基于JMM模型理解可见性的原理
从JMM提供的抽象模型上来说,它保证可见性的方式如下,假设我们有一个volatile共享变量a,值为1,线程1和线程2协作的操作如下
- 由于是
volatile变量,所以线程1就不会对应将本地内存设置为无效,直接从主存中获取,并加载到主存中。 - 然后线程1将值修改为2,由于需要保证可见性,所以
JMM会把这个变量写如主存中。 - 线程2读取,同样因为
volatile修饰的原因,不走本地内存,直接从主存中获取,从而保证缓存一致性。

# volatile保证可见性代码示例
代码如下所示,分别给num变量加上volatile和删除volatile
public class VolatileModify {
private volatile static int num = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (num == 0) {
}
System.out.println("num已被修改为:1" );
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
System.out.println("t2修改num为1" );
});
t1.start();
t2.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
输出结果
/**
* 加volatile关键字
* t2修改num为1
* num已被修改为:1
*/
2
3
4
5
而不加volatile,则t1无法感知改变就会一直走CPU Cache中的值,导致死循环。
/**
* 不加volatile
*t2修改num为1
*/
2
3
4
# volatile可以禁止指令重排序
volatile不仅可以保证可见性,还可以避免指令重排序。如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。
双重校验锁实现对象单例(线程安全):
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
- 为
uniqueInstance分配内存空间 - 初始化
uniqueInstance - 将
uniqueInstance指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 1 执行了 1 和 3,此时 线程2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

# volatile无法保证原子性
volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。
# 无法保证原子性代码示例
public class VolatoleAtomicityDemo {
public volatile static int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
VolatoleAtomicityDemo volatoleAtomicityDemo = new VolatoleAtomicityDemo();
for (int i = 0; i < 5; i++) {
threadPool.execute(() -> {
for (int j = 0; j < 500; j++) {
volatoleAtomicityDemo.increase();
}
});
}
// 等待1.5秒,保证上面程序执行完成
Thread.sleep(1500);
System.out.println(inc);
threadPool.shutdown();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
正常情况下,运行上面的代码理应输出 2500。但真正运行了上面的代码之后,每次输出结果都小于 2500。也就是说,如果 volatile 能保证 inc++ 操作的原子性的话。每个线程中对 inc 变量自增完之后,其他线程可以立即看到修改后的值。5 个线程分别进行了 500 次操作,那么最终 inc 的值应该是 5*500=2500
实际上,inc++ 其实是一个复合操作,包括三步:
- 读取 inc 的值。
- 对 inc 加 1。
- 将 inc 的值写回内存。
volatile 是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现:
- 线程 1 对
inc进行读取操作之后,还未对其进行修改。线程 2 又读取了inc的值并对其进行修改(+1),再将inc的值写回内存。 - 线程 2 操作完毕后,线程 1 对
inc的值进行修改(+1),再将inc的值写回内存。
这也就导致两个线程分别对 inc 进行了一次自增操作后,inc 实际上只增加了 1。
其实,如果想要上面的代码运行正确,可以利用 synchronized、Lock或者AtomicInteger。
使用 synchronized 改进:
public synchronized void increase() {
inc++;
}
2
3
使用 AtomicInteger 改进:
public AtomicInteger inc = new AtomicInteger();
public void increase() {
inc.getAndIncrement();
}
2
3
4
5
使用 ReentrantLock 改进:
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
try {
inc++;
} finally {
lock.unlock();
}
}
2
3
4
5
6
7
8
9