在《深入理解Java虚拟机》一书中有这样一段代码:
public class VolatileTest { public static volatile int race = 0; public static void increase() { race++; } private static final int THREADS_COUNT=20; public static void main(String[] args) { Thread[] threads = new Thread[THREADS_COUNT]; for(int i = 0; i < THREADS_COUNT; i++){ new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { increase(); } } }).start(); } //等待所有累加线程都结束 while(Thread.activeCount()>1) Thread.yield(); System.out.println(race); } }
你看到这段代码的第一反应是什么?
是不是关注点都在 volatile 关键字上。
甚至马上就要开始脱口而出:volatile 只保证可见性,不保证原子性。而代码中的 race++
不是原子性的操作,巴拉巴拉巴拉...
反正我就是这样的:
当他把代码发给我,我在 idea 里面一粘贴,然后把 main 方法运行起来后,神奇的事情出现了。
这个代码真的没有执行到输出语句,也没有任何报错。
看起来就像是死循环了一样。
不信的话,你也可以放到你的 idea 里面去执行一下。
等等......
死循环?
代码里面不是就有一个死循环吗?
//等待所有累加线程都结束 while(Thread.activeCount()>1) Thread.yield();
这段代码能有什么小心思呢?看起来人畜无害啊。
但是程序员的直觉告诉我,这个地方就是有问题的。
活跃线程一直是大于 1 的,所以导致 while 一直在死循环。
算了,不想了,先 Debug 看一眼吧。
Debug 了两遍之后,我才发现,这个事情,有点意思了。
因为 Debug 的情况下,程序竟然正常结束了。
啥情况啊?
分析一波走起。
为啥停不下来?
我是怎么分析这个问题的呢。
我就把程序又 Run 了起来,控制台还是啥输出都没有。
我就盯着这个控制台想啊,会是啥原因呢?
这样干看着也不是办法啊。
反正我现在就是咬死这个 while 循环是有问题的,所以为了排除其他的干扰项。
我把程序简化到了这个样子:
public class VolatileTest { public static volatile int race = 0; public static void main(String[] args) { while(Thread.activeCount()>1) Thread.yield(); System.out.println("race = " + race); } }
运行起来之后,还是没有执行到输出语句,也就侧面证实了我的想法:while 循环有问题。
而 while 循环的条件就是 Thread.activeCount()>1
朝着这个方向继续想下去,就是看看当前活跃线程到底有几个。
于是程序又可以简化成这样:
直接运行看到输出结果是 2
用 Debug 模式运行时返回的是 1。
对比这运行结果,我心里基本上就有数了。
先看一下这个 activeCount 方法是干啥的:
注意看画着下划线的地方:
返回的值是一个 estimate。
estimate 是啥?
你看,又在我这里学一个高级词汇。真是 very good。
返回的是一个预估值。
为什么呢?
因为我们调用这个方法的一刻获取到值之后,线程数还是在动态变化的。
也就是说返回的值只代表你调用的那一刻有几个活跃线程,也许当你调用完成后,有一个线程就立马嗝屁了。
所以,这个值是个预估值。
这一瞬间,我突然想到了量子力学中的测不准原理。
你不可能同时知道一个粒子的位置和它的速度,就像在多线程高并发的情况下你不可能同时知道调用 activeCount 方法得到的值和你要用这个值的时刻,这个值的真实值是多少。
你看,刚学完英语又学量子力学。
好了,回到程序里面。
虽然注释里面说了返回值是 estimate 的,但是在我们的程序中,并不存在这样的问题。
看到 activeCount 方法的实现之后:
public static int activeCount() { return currentThread().getThreadGroup().activeCount(); }
我又想到,既然在直接 Run 的情况下,程序返回的数是 2,那我看看到底有那些线程呢?
其实最开始我想着去 Debug 一下的,但是 Debug 的情况下,返回的数是 1。我意识到,这个问题肯定和 idea 有关,而且必须得用日志调试大法才能知道原因。
于是,我把程序改成了这样:
直接 Run 起来,可以看到,确实有两个线程。
一个是 main 线程,我们熟悉。
一个是 Monitor Ctrl-Break 线程,我不认识。
但是当我用 Debug 的方式运行的时候,有意思的事情就发生了: