背景
今天技术群里有人抛出来一个很有意思的问题,如下代码,线程A会进入死循环吗?
public class Test {
static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
System.out.println("Thread-A start");
int x = 1;
while (flag) {
System.out.println(">>>>>");
x++;
}
System.out.println("Thread-A end, x:" + x);
});
threadA.start();
Thread.sleep(100);
System.out.println("Thread-main set flag = " + flag);
flag = false;
System.out.println("Thread-main set flag = " + flag);
}
}
初步分析
上述代码的大概逻辑是,开启了一个线程A,在线程A中写了一个循环,当 flag==true 的时候不断循环打印。然后在主线程中将 flag 置为 false,以此使线程A停止循环。
先看下 flag 变量的定义:
static boolean flag = true;
并没有使用 volatile 关键字,根据内存模型和可见性,线程A在将flag变量读取到工作内存后,其他线程对于flag变量的修改,并不会对线程A可见。因此我们推断,线程A会进入死循环。
但是,真的是这样吗?不妨把上面的代码运行一遍,我们发现,线程A并没有进入死循环,程序在一通控制台输出后退出了。。

进一步分析
既然没有加 volatile 关键字,那这段代码又是如何保证可见性的呢?
我们知道,对于可见性而言,除了 volatile 还可以通过 synchronized 实现,而 System.out.println() 方法源码实际是这样的:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
原本我对 synchronized 的理解是,仅会将 synchronized 同步代码块中更改的变量同步回主内存。查阅资料发现,在Doug Lea的一篇文章中有对 synchronized 可见性的描述:

大概意思是说:释放锁会将当前锁定线程的工作内存中已写入的数据刷回到主内存中,而获取锁定会强制重新加载主内存中的数据。虽然锁操作只对同步方法和同步代码块这一块起到作用,但是影响的却是线程执行操作所使用的所有字段。
总结
综上所述,由于 System.out.println() 方法的 synchronized 关键字,使线程工作内存和主内存做了同步。
在主线程中,我们在设置完flag变量后,调用了一次打印,此时主线程工作内存中数据被同步到主内存中。然后线程A循环打印过程中,又将强制读取了主内存中的数据,所以读取到了flag的变化。