笔者在调试与实现《深入理解Java虚拟机》一书中的代码时发现,本人使用的IDEA工具与书中所用Eclips在开辟线程时略有不同
例1:
public class DeadLoopClass {
static {
//此处必须使用if判断,否则编译器将拒绝编译
if (true){
System.out.println(Thread.currentThread()+"init DeadLoopClass");
while (true){} //模拟长时间操作
}
}
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread()+"start");
DeadLoopClass deadLoopClass = new DeadLoopClass();
System.out.println(Thread.currentThread()+"end");
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
从类加载过程(三)而来的读者看到的应该是以上的代码,这段代码和书中并没有任何不同,但是运行结果却有着差异。
在笔者使用的IDEA工具中得到的运行结果为:
Thread[main,5,main]init DeadLoopClass
而在书中描述的Eclipse环境下的运行结果为 :
Thread[Thread-0,5,main]start
Thread[Thread-0,5,main]start
Thread[Thread-0,5,main]init DeadLoopClass
这里可以看到,Eclipse中三条线程都被启动了,然后其中一条进入了死循环模拟长时间运行;而在IDEA中只有一条线程被启动,现在我们注释掉while(true){},运行结果如下
Thread[main,5,main]init DeadLoopClass
Thread[Thread-0,5,main]start
Thread[Thread-1,5,main]start
Thread[Thread-0,5,main]end
Thread[Thread-1,5,main]end
这此,三条线程自然都启动了,但是可以看到另外两个线程的名字并非Eclipse中的main,而是分别编号为0和1的子线程。有心debug的读者可以一试,在while (true){}和Runnable runnable = new Runnable()处打上断点,会发现,主线程启动时并没有停下,直接进入了死循环中,两个子线程根本就没有被创建,而名为main的主线程则是在启动时就被创建!
关于主线程和子线程的优先级问题,在《深入理解计算机操作系统》(CSAPP)一书中有着明确的解释,主线程的优先级永远高于子线程,也就是说对于常见的操作系统,只有在主线程启动之后才会执行由其开辟的子线程。
当笔者调试代码时认为jvm也应当遵循上述规则,书中作者可能是将此规则看作常识故此没有给出解释。然而不然,在笔者调试例2时出现了新的状况。
例2:
//volatile变量自增运算
public class VolatileTest {
//定义volatile变量
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.length; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
//循环自增volatile变量
for (int i = 0 ; i <10000 ; i++){
increase();
}
}
});
//开启线程
threads[i].start();
}
//等待所有线程累加结束
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(race);
}
}
从 Java内存模型(一) 而来的读者见到的应该是以上代码,对于没有阅读和尚未读到该节的读者,只需了解以上代码是对 volatile变量的运算 即可。这里我们重点讨论while (Thread.activeCount() > 2)这行代码。
书中作者使用的是:
while (Thread.activeCount() > 1)
意为当前线程组中线程数量大于1时,当前线程放弃CPU,这样以保证每个线程都能运算完毕。然而笔者在使用IDEA工具运行该代码时发现控制台一直没有打印数据,并且程序依然在运行,在进行debug时,果然产生了令人诧异的结果。
首先,我们在for (int i = 0; i < threads.length; i++) 处进行打上断点并单步执行,控制台输出了结果,程序运行完毕,但是结果并非书中所言出现volatile变量同步错误导致的数值减小,而是正确的结果200000。
关于运行结果这一点,我们先不讨论,但是为什么此时的程序能执行完毕呢。
这次,我们修改代码为while (Thread.activeCount() > 1),并在此处打上断点,再次单步运行,有意思的事情发生了,这次程序进入了死循环之中,无论如何操作,
Thread.activeCount() 得到的值总是大于1。
这是因为,对于我们的计算机(不考虑单线程CPU的机器)而言,执行以上代码需要极短(近乎无法感知)的时间。
当我们直接运行时,可以变相的理解为循环中20条线程的开辟是并行(同时发生)的。这时线程组中至少有一条子线程(循环中开辟的20条线程),再加上例1中提到的运行时自动创建的一条名为main的主线程守护线程,此线程组中至少会存在2条线程,因此进入了死循环。
当我们使用debug调试时,相当于控制了子线程开辟的时间,使得子线程的开辟在宏观上变成了并发(其实已经是每次只开辟一条线程了),因此线程组中每次都有且仅有2条线程,这样就保证了没条线程既可以运行完毕,又可以退出循环。因此这种一次只有一条子线程运算的情况下,自然不会出现volatile变量的同步错误问题。
虚拟机监视工具的报告
虽然解开了为什么例2中的程序会进入死循环的问题,但是我们至此对于线程组中多出一条名为main的线程仍是以推测的态度来解释的。
出于好奇,笔者使用了cmd编译例2中条件为while (Thread.activeCount() > 1) 的代码并运行,结果和书中相同,并没有出现IDEA工具中进入死循环的情况。
这里我们使用jdk中提供的jvisualvm工具(可以在jdk的bin目录下找到,可以根据个人需要安装不同的插件)进行监视,发现了如下情况:


从图中可以看出,名为main的主线程是第一个被创建的,并且一直处于运行状态,如果查看完整的线程dump还可以看到,main线程甚至比CompilerThread编译线程的创建还要早。
在笔者的线程dump中0~4号编译线程的prio=9 ,os_prio=2其优先级要比主线程低。但值得注意的是main主线程并没有daemon,即主线程为实时线程,而非守护线程。
以上为笔者生成的dump文件,有兴趣的读者可以自行下载和自己的dump文件进行比对。
至此,我们可以得出结论,通过IDEA工具运行的程序会在程序启动时创建名为main的主线程(实时线程),而使用cmd或eclipse却不会创建这样的一条线程。
参考文献
这里,笔者不再对eclipse进行单独的调试,有兴趣的读者可以自己尝试。调试代码可是使用本文中所提供的代码,也可以参考 CSDN用户
xiaolinzi007 文章所提供的代码:
【转载】版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
ps://blog.csdn.net/xiaolinzi007/article/details/44487851
- 《深入理解Java虚拟机》
- 《深入理解计算机操作系统 》