《深入理解java虚拟机》volatile代码案例的一个bug?

背景:
最近在读周志明的《深入理解 java虚拟机》第二版,发现第367页的 volatile演示例子(代码清单 12-1)无法运行出相应结果,在此记录下原因及解决办法 ~

注:代码运行环境为 win64 - jdk1.8 - IntelliJ IDEA


书中代码如下:

import java.util.Map;

/**
 * @Author: yuanj
 * @Date: 2018/11/27 14:44
 */
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) throws Exception {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        //等待所有累加线程都结束
        while (Thread.activeCount() > 1) {         
            Thread.yield();
        }
        System.out.println(race);
    }
}

直接运行该代码会发现程序无法结束,陷入无限循环;
简单分析很大可能是while (Thread.activeCount() > 1)语句中的activeCount始终不为1,按我们的理解,在main主线程中启动20个线程依次执行任务,等待所有累加线程执行完毕,应该只剩下一个main线程没错,但程序运行的结果说明事实也许并不是这样!

排查:
我们在while循环中打印Thread.activeCount()所有活跃线程数量,发现为线程数为2,果然有一个我们不知道的线程隐藏其中!

java.lang.Thread.activeCount() 方法返回活动线程的当前线程的线程组中的数量

java.lang.Thread.getAllStackTraces() 方法返回堆栈跟踪的所有活动线程的线程。映射上的键是线程,每个映射的值是一个StackTraceElement数组,表示相应的线程的堆栈转储

打印当前线程:

while (Thread.activeCount() > 1) {
	//打印java启动时创建了哪些线程
	for (Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet()) {
		System.out.println("========" + entry.getKey());
	}
	//打印当前活跃线程列表
	Thread.currentThread().getThreadGroup().list();
}

结果如下:
在这里插入图片描述
在这里插入图片描述

果不其然!Thread.activeCount()中有一个Monitor Ctrl-Break隐藏线程在运行,并且java程序启动时还创建了Signal Dispatcher 、Finalizer 、Reference Handler 、Attach Listener等一系列线程!

所以我们只需要把Thread.activeCount() > 1改为>2即可!


扩展一:这些线程都是干嘛的呢?

Finalizer

这个线程也是在main线程之后创建的,其优先级为10,主要用于在垃圾收集前,调用对象的finalize()方法;关于Finalizer线程的几点:

  1. 只有当开始一轮垃圾收集时,才会开始调用finalize()方法;因此并不是所有对象的finalize()方法都会被执行
  2. 该线程也是daemon线程,因此如果虚拟机中没有其他非daemon线程,不管该线程有没有执行完finalize()方法,JVM也会退出
  3. JVM在垃圾收集时会将失去引用的对象包装成Finalizer对象(Reference的实现),并放入ReferenceQueue,由Finalizer线程来处理;最后将该Finalizer对象的引用置为null,由垃圾收集器来回收
  4. JVM为什么要单独用一个线程来执行finalize()方法呢?如果JVM的垃圾收集线程自己来做,很有可能由于在finalize()方法中误操作导致GC线程停止或不可控,这对GC线程来说是一种灾难

Attach Listener

Attach Listener线程是负责接收到外部的命令,而对该命令进行执行的并且把结果返回给发送者。通常我们会用一些命令去要求jvm给我们一些反馈信息,如:java -version、jmap、jstack等等。如果该线程在jvm启动的时候没有初始化,那么,则会在用户第一次执行jvm命令时,得到启动。

Signal Dispatcher

前面我们提到第一个Attach Listener线程的职责是接收外部jvm命令,当命令接收成功后,会交给signal dispather线程去进行分发到各个不同的模块处理命令,并且返回处理结果。signal dispather线程也是在第一次接收外部jvm命令时,进行初始化工作。

Reference Handler

JVM在创建main线程后就创建Reference Handler线程,其优先级最高,为10,它主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题。

Monitor Ctrl-Break

监控Ctrl-Break中断信号

扩展二:问题出现的本质在于IDEA?

直接采用java命令行或者eclipse,再或者IDEAdebug方式执行,结果都是1!
但是用IDEArun方式执行却是2!

原来IDEA的run一个程序时,会通过-javaagent参数设置一个自己的监视器,它是通过反射的方式在执行用户程序之前,在当前线程的线程组之中开了一个Ctrl Break的Monitor的Socket线程,去做监听,所以才会产生这种问题!

发布了73 篇原创文章 · 获赞 373 · 访问量 42万+

猜你喜欢

转载自blog.csdn.net/Abysscarry/article/details/84576677