对共享可变数据不加同步,虚拟机可能会搞死你

159 阅读3分钟
原文链接: mp.weixin.qq.com

大家都知道,在Java中,除非变量的类型是 long 或 double,否则对于变量的读取和写入操作都是原子性的。也就是说,即使多个线程同时访问并修改变量,也不会有线程安全的问题。

于是,有人可能就会觉得,既然对变量(除long或double)的读取和写入操作是原子性的,那我们是不是在多线程情况下,也可以不考虑使用同步,直接对这些变量就行读写操作呢,因为如果要对这些简单的操作加同步,反而会影响代码性能。

这种想法是不是对的呢?我们来看看下面的代码:

public class StopThread {    private static boolean stopRequested;    public static void main(String[] args) throws InterruptedException {        Thread backgroundThread = new Thread(() -> {          int i = 0;          while (!stopRequested)              i++;        });        backgroundThread.start();        TimeUnit.SECONDS.sleep(1);        stopRequested = true;    }}

上面的代码实现的功能是:主线程启动backgroundThread 线程,然后自己停止一秒钟,之后主线程将 stopRequested 设置为 true,从而导致后台线程的循环终止,程序结束。

代码看上去没有上面问题,然而,在我的机器上,程序永远不会终止:后台线程永远循环!

那么问题出在哪里呢?我们在主线程中做了stopRequested = true; ,变成true之后,backgroundThread 线程在while循环中判断到这一变化不是应该停止循环结束程序吗?怎么就能死循环了呢?

其实问题在于在缺乏同步的情况下,无法保证后台线程何时(如果有的话)看到主线程所做的 stopRequested 值的更改。在缺乏同步的情况下,虚拟机会优化 backgroundThread 中的代码,将:

while (!stopRequested)    i++;

优化成:

if (!stopRequested)    while (true)        i++;

这种操作可以说简直是丧心病狂……,我的天,由于stopRequested 的判断被提到了while循环之外,导致了我们的程序进入死循环无法停止。罪魁祸首居然是因为虚拟机的优化导致的问题。而这点确实很不容易发现。

那么怎么觉得这个问题呢?既然是因为缺乏同步导致的主线程stopRequested 修改无法被backgroundThread 线程发现,那么我们就给他加上同步,代码如下:

public class StopThread {    private static boolean stopRequested;    private static synchronized void requestStop() {        stopRequested = true;    }    private static synchronized boolean stopRequested() {        return stopRequested;    }    public static void main(String[] args) throws InterruptedException {        Thread backgroundThread = new Thread(() -> {          int i = 0;          while (!stopRequested())            i++;        });        backgroundThread.start();        TimeUnit.SECONDS.sleep(1);        requestStop();    }}

我们通过对stopRequested 的读写添加了同步,这样,就可以让主线程对stopRequested 的修改被backgroundThread 线程看到,同时虚拟机也不再会出现上面的优化代码,而是按照正常的逻辑走,每次访问stopRequested 或修改都需要获取到锁才能操作。

通过上面的修改后,强哥的机器上代码终于能够正常的终止了。可是要去修改一个变量就要去手动编写对这个变量读写的加锁代码也好麻烦,有没有更好的方法呢?

当然有了,如果 stopRequested 声明为 volatile,则可以省略 stopRequested 的第二个版本中的锁。它不那么冗长,而且性能可能更好。虽然 volatile 修饰符不执行互斥,但它保证任何读取字段的线程都会看到最近写入的值:

public class StopThread {    private static volatile boolean stopRequested;    public static void main(String[] args) throws InterruptedException {        Thread backgroundThread = new Thread(() -> {            int i = 0;            while (!stopRequested)                i++;        });            backgroundThread.start();        TimeUnit.SECONDS.sleep(1);        stopRequested = true;    }}

所以,对于共享可变数据,虽然对它的读写操作是原子性的,但是却不能保证由一个线程编写的值对另一个线程可见。因此,如果出现线程之间能可靠通信以及实施互斥,同步是所必需的。

强哥叨逼叨

叨逼叨编程、互联网的

见解和新鲜事