首先抛出一个问题:“volatile 这个关键字有什么作用?”。常见的回答或许有两种:
- 一种是把 volatile 当成一种锁机制,认为给变量加上了 volatile,就好像是给函数加了 sychronized 关键字一样,不同的线程对于特定变量的访问会去加锁;
- 另一种是把 volatile 当成一种原子化的操作机制,认为加了 volatile 之后,对于一个变量的自增的操作就会变成原子性的
事实上,这两种理解都是完全错误的。下面我们通过三个连续的小 demo 来看看 volatile 到底有什么用。
简单来说,使用 volatile,将会强制我们对成员变量的读/写都直接与内存
将会强制所有线程都去堆内存中读取成员变量最新的值,而不是读取自己线程工作空间的缓存。
注:volatile 并不能保证多个线程共同修改成员变量时所带来的不一致问题,也就是说 volatile 不能替代 synchronized
volatile 作用示例
我们先来一起看一段 Java 程序。这是一段经典的 volatile 代码,来自知名的 Java 开发者网站 dzone.com,后续我们会修改这段代码来进行各种小实验。
1.代码示例一
public class VolatileTest {
// volatile修饰
private static volatile int COUNTER = 0;
public static void main(String[] args) {
// 启动ChangeListener线程
new ChangeListener().start();
// 启动ChangeMaker线程
new ChangeMaker().start();
}
// ChangeListener 线程
// 监听COUNTER变量,当COUNTER发生变化时就将变化的值打印出来
static class ChangeListener extends Thread {
@Override
public void run() {
// threadValue,用于对比 COUNTER 是否发生变化
// 将 COUNTER 的值赋给 threadValue 变量
int threadValue = COUNTER;
while (threadValue < 5){
// 当 COUNTER 发生变化,打印
if( threadValue!= COUNTER){
System.out.println("Got Change for COUNTER : " + COUNTER + "");
threadValue= COUNTER;
}
}
}
}
// ChangeMaker 线程
// 监听COUNTER变量,当COUNTER小于5时就每个500毫秒将COUNTER自增1
static class ChangeMaker extends Thread{
@Override
public void run() {
int threadValue = COUNTER;
while (COUNTER <5){
System.out.println("Incrementing COUNTER to : " + (threadValue+1) + "");
// COUNTER++
COUNTER = ++threadValue;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
程序的输出结果并不让人意外。ChangeMaker 函数会一次一次将 COUNTER 从 0 增加到 5。因为这个自增是每 500 毫秒一次,而 ChangeListener 去监听 COUNTER 是忙等待的,所以每一次自增都会被 ChangeListener 监听到,然后对应的结果就会被打印出来
Incrementing COUNTER to : 1
Got Change for COUNTER : 1
Incrementing COUNTER to : 2
Got Change for COUNTER : 2
Incrementing COUNTER to : 3
Got Change for COUNTER : 3
Incrementing COUNTER to : 4
Got Change for COUNTER : 4
Incrementing COUNTER to : 5
Got Change for COUNTER : 5
2.代码示例二
这个时候,如果我们把上面的程序小小地修改一行代码,把我们定义 COUNTER 这个变量的时候,设置的 volatile 关键字给去掉,会发生什么事情呢?
private static int COUNTER = 0;
结果是 ChangeMaker 还是能正常工作的,每隔 500ms 仍然能够对 COUNTER 自增 1,但是 ChangeListener 不再工作了。在 ChangeListener 眼里,它似乎一直觉得 COUNTER 的 值还是一开始的 0。似乎 COUNTER 的变化,对于我们的 ChangeListener 彻底“隐身”了。
Incrementing COUNTER to : 1
Incrementing COUNTER to : 2
Incrementing COUNTER to : 3
Incrementing COUNTER to : 4
Incrementing COUNTER to : 5
3.代码示例三
这个有意思的小程序还没有结束,我们可以再对程序做一些小小的修改。我们不再让 ChangeListener 进行完全的忙等待,而是在 while 循环里面,小小地等待上 5 毫秒,看看 会发生什么情况。
static class ChangeListener extends Thread {
@Override
public void run() {
int threadValue = COUNTER;
while ( threadValue < 5){
if( threadValue!= COUNTER){
System.out.println("Sleep 5ms, Got Change for COUNTER : " + COUNTER + "");
threadValue= COUNTER;
}
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
又一个令人惊奇的现象要发生了。虽然我们的 COUNTER 变量,仍然没有设置 volatile 这个关键字,但是我们的 ChangeListener 似乎“睡醒了”。在通过 Thread.sleep(5) 在每个循环里“睡上“5 毫秒之后, ChangeListener 又能够正常取到 COUNTER 的值了。
Incrementing COUNTER to : 1
Sleep 5ms, Got Change for COUNTER : 1
Incrementing COUNTER to : 2
Sleep 5ms, Got Change for COUNTER : 2
Incrementing COUNTER to : 3
Sleep 5ms, Got Change for COUNTER : 3
Incrementing COUNTER to : 4
Sleep 5ms, Got Change for COUNTER : 4
Incrementing COUNTER to : 5
Sleep 5ms, Got Change for COUNTER : 5
=> volatile 作用
这些有意思的现象,其实来自于我们的 Java 内存模型以及关键字 volatile 的含义。那 volatile 关键字究竟代表什么含义呢?它会确保我们对于这个变量的读取和写入,都一定会同步到主内存里,而不是从线程的 Cache 里面读取。该怎么理解这个解释呢?我们通过刚才的例子来进行分析。
- 刚刚第一个使用了 volatile 关键字的例子里,因为所有数据的读和写都来自主内存。那么自然地,我们的 ChangeMaker 和 ChangeListener 之间,看到的 COUNTER 值就是一样的。
- 到了第二段进行小小修改的时候,我们去掉了 volatile 关键字。这个时候, ChangeListener 又是一个忙等待的循环,它尝试不停地获取 COUNTER 的值,这样就会从当前线程的“Cache”里面获取。于是,这个线程就没有时间从主内存里面同步更新后的 COUNTER 值。这样,它就一直卡死在 COUNTER=0 的死循环上了。
- 而到了我们再次修改的第三段代码里面,虽然还是没有使用 volatile 关键字,但是短短 5ms 的 Thead.Sleep 给了这个线程喘息之机。既然这个线程没有这么忙了,它也就有机会把新的数据从主内存同步到自己的高速缓存里面了。于是,ChangeListener 在下一次查 看 COUNTER 值的时候,就能看到 ChangeMaker 造成的变化了。
虽然 Java 内存模型是一个隔离了硬件实现的虚拟机内的抽象模型,但是它给了我们一个很好的“缓存同步”问题的示例。也就是说,如果我们的数据,在不同的线程或者 CPU 核里面去更新,因为不同的线程或 CPU 核有着自己各自的缓存,很有可能在 A 线程的更新,到 B 线程里面是看不见的。