volatile 关键字
现在有如下两个代码:
public class MyThread extends Thread{
public static int a = 0;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程开始执行,当前 a = " + a);
a = 1;
System.out.println("线程执行结束,当前 a = " + a);
}
}
public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
while(true){
if(MyThread.a == 1){
System.out.println("检测到 a = 1");
break;
}
}
}
}
那么你认为,如果我运行主类,得到的结果是什么?
根据我们先前的知识,我们肯定会认为,代码运行后,首先创建了 t 线程,然后 t 线程启动,t 线程与主线程同时执行,此时主线程处于无限循环,等待并监听 MyThread 中的共享成员 a 的值,当线程 t 睡眠 1 秒后修改 a 的值变为 1,之后主线程监听到 a 的值变成 1,跳出循环,结束整个程序。也就是说整个程序依次打印输出:
线程开始执行,当前 a = 0
线程开始执行,当前 a = 1
检测到 a = 1
然后程序结束运行。这是我们根据代码思考出来的运行结果,但事实真的是这样吗?我们尝试一下:

我们发现,程序只只输出了线程 t 中的内容,输出完成后程序并没有结束,而是仍然在一直运行,即使我们等了很久很久,也没有停止运行,这是为什么呢?
其实,这种问题,我们称之为多线程的可见性,指的是一个线程对共享变量的修改,其他线程能否立即看到。
可见性的原因产生有很多,当前这种情况主要是因为:现代 CPU 一般会将变量值缓存在寄存器或本地缓存中,而不是每次都从主内存读取。
简单来说,对于 static 修饰的这种共享变量,为了避免多个线程同时直接去修改主内存中的数据而导致一系列的问题,Java 会在读取这个共享变量的时候,先复制一份这个变量的副本到当前线程的工作内存中,如果当前线程不对这个副本的数据进行修改,则不会重新进行获取;只有当前线程对这个副本的数据进行了修改,线程才会去同步这个修改到这个真正的共享变量中去。
所以,上面的问题就很好分析了,这是因为 t 线程首先睡眠了 1 秒,这保证了主线程先去执行了 if(MyThread.a == 1),这使得主线程获得了一份 a 变量的副本,a 的值是 0。当 t 线程睡眠结束后把 a 改成 1 后,虽然同步到了共享变量里,但主线程因为没有对 a 进行过修改,所以主线程一直没有去重新获取 a 的最新数据,一直都是用的原来 a = 0 的副本,这就导致了主线程的死循环。
那么我们容易想到,我们重新让主线程获取一下 a 的值不就行了?这显然是可以的,那么如何重新获得 a 的值呢?其实有以下几种方法:
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
while(true){
Thread.sleep(1);
if(MyThread.a == 1){
System.out.println("检测到 a = 1");
break;
}
}
}
}
我们可以在 while 里面加上一句 Thread.sleep(毫秒数),其中这个 毫秒数 可以任意填写,这不重要,重要的是我们使用了 sleep 这个方法,首先会让主线程进行阻塞,一旦阻塞结束进入就绪态后,就会重新获取共享变量中的数据,结果如下:

这次我们可以看到,程序达到了我们的运行的目的,并且程序也正常结束了。除此之外,我们还有别的方法:
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
while(true){
synchronized (Main.class) {}
if(MyThread.a == 1){
System.out.println("检测到 a = 1");
break;
}
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
while(true){
Thread.currentThread().resume();
if(MyThread.a == 1){
System.out.println("检测到 a = 1");
break;
}
}
}
}
这两种方法分别是运用了同步代码块运行结束后会重新获取共享变量数据的特点和强制刷新线程后重新获取共享变量的数据特点,且第二种方法已经被淘汰了。
那么有没有直接解决共享变量数据不一致问题的方法?当然有,就是 volatile 关键字,只要共享变量被 volatile 修饰,线程在写入时会将值刷新到主内存,读取时会强制从主内存获取最新值,然后存入线程的工作内存(副本)中使用,而不是一直使用旧的本地副本。
如下:
public class MyThread extends Thread{
public static volatile int a = 0;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程开始执行,当前 a = " + a);
a = 1;
System.out.println("线程执行结束,当前 a = " + a);
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
while(true){
if(MyThread.a == 1){
System.out.println("检测到 a = 1");
break;
}
}
}
}

可以看到成功达到了要求。
当然,volatile 对于未被 static 修饰的普通成员也有效果,大家可以自行尝试。
实际上,volatile 还可以禁止指令重排,虽然重排对多线程的执行影响远远不如随机性大,但这个作用也需要记一下。