本文重点解读JDK中的重要概念——线程组(ThreadGroup)的源码。

1.线程组的树状结构

我们先看一下java.lang.ThreadGroup的成员变量:

image-20230523203618607

通过JDK源码可以看到,每个ThreadGroup对象拥有一个父线程组parent,同时每个ThreadGroup对象也拥有自己的子线程组集合group[]和子线程集合threads[]

这样,JDK的ThreadGroup本质上形成了一个线程组和线程的树状结构。如下图:

image-20230523203932254

2.决定线程组父子关系的时机:构造函数

ThreadGroup提供了4个构造函数(其中2个是public类型以供程序猿调用),如下图所示:

image-20230524090234413

这4个构造函数的调用链关系如下:

image-20230524094642711

我们接下来逐一分析它们的源码。

2.1.ThreadGroup()

分析第1个无参构造函数,可知:

  • 它仅被JVM调用
  • 它创建名为system的线程组
  • 它的父线程组为null

image-20230524090656391

2.2.ThreadGroup(String name)

分析第2个构造函数,可知:

  • 它调用了第3个构造函数
  • 它获取了当前线程所在的线程组,作为自己的父线程组

image-20230524091312700

2.3.ThreadGroup(ThreadGroup parent, String name)

分析第3个构造函数,可知:

  • 它调用了第3个构造函数
  • 它调用了checkParentAccess方法,检查了父线程组parent的权限

image-20230524091449229

2.4.ThreadGroup(Void unused, ThreadGroup parent, String name)

分析第4个构造函数,可知:

  • 在这个构造函数中,调用add方法,将传入的线程组对象设置为父线程组。

image-20230524091708771

2.5.add(ThreadGroup g)

此方法就是构建线程组父子关系的关键代码:

  • 813行:当前线程组对象的子线程组集合为空,则创建长度为4的子线程组集合

  • 815行:如果当前线程组对象的子线程组集合长度已经满了,则将子线程组集合扩容为2倍

    • 注:JDK源码中有很多扩容代码,在空间和时间上进行性能平衡,值得学习
  • 817行:将传入的线程组g,加入到当前线程组对象的子线程组集合

  • 821行:将子线程组集合的游标增1

image-20230524094843439

2.6.测试

我们可以写一段测试代码:

  • 我们借助getParent()方法,获得指定线程组的父线程组
  • 我们借助list()方法,打印当前进程中的线程组的树状结构

image-20230524093324551

上述代码的控制台输出如下:

  • 上述代码就形成了1个线程组树:system线程组>main线程组>线程组1>线程组2

image-20230524093905263

3.决定线程父子关系的时机:add

3.1.触发

  • Thread.start()方法会调用ThreadGroup.add(Thread t)方法,目的是将该线程加入对应线程组的子线程组集合

image-20230524100028755

3.2.ThreadGroup.add(Thread t)

此方法就是构建线程和线程组父子关系的关键代码:

  • 892行:当前线程组对象的子线程集合为空,则创建长度为4的子线程集合

  • 894行:如果当前线程组对象的子线程集合长度已经满了,则将子线程集合扩容为2倍

  • 896行:将传入的线程t,加入到当前线程组对象的子线程集合

  • 900行:将子线程集合的游标增1

  • 906行:将未启动的线程数减1

image-20230524100637548

3.3.测试

我们的测试代码如下:

  • 创建3个线程:线程1、线程2、线程3
  • 线程1:未指定线程组,Thread的构造函数会找当前线程对象的父线程对象所属的线程组,指定线程1要加入到该线程组
    • 注:此处不展开,读者可通过Thread构造函数自行分析
  • 线程2:指定线程2要加入到线程组1
  • 线程3:指定线程3要加入到线程组2
  • start:线程1、线程2、线程3启动,触发了3个线程真正地加入到对应的线程组

image-20230524101258085

上述代码的控制台输出如下:

  • 上述代码就形成了1个线程组和线程的树状结构:

image-20230524102208868

4.完整测试代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Main {
    public static void main(String[] args) {
        ThreadGroup threadGroup1 = new ThreadGroup("线程组1");
        ThreadGroup threadGroup2 = new ThreadGroup(threadGroup1, "线程组2");

        Thread thread1 = new Thread(() -> {
            System.out.println("..." + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "线程1");
        Thread thread2 = new Thread(threadGroup1, () -> {
            System.out.println("..." + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "线程2");
        Thread thread3 = new Thread(threadGroup2, () -> {
            System.out.println("..." + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "线程3");

        thread1.start();
        thread2.start();
        thread3.start();

        // getParent/parentOf
        ThreadGroup threadGroupMain = threadGroup1.getParent();
        System.out.println(threadGroupMain.getName());
        System.out.println(threadGroupMain.parentOf(threadGroup1));

        //  activeCount/activeGroupCount
        System.out.println(threadGroup1.activeCount());
        System.out.println(threadGroup1.activeGroupCount());

        // list
        ThreadGroup threadGroupSystem = threadGroupMain.getParent();
        threadGroupSystem.list();
    }
}

5.参考

1
2
java/lang/ThreadGroup.java
java/lang/Thread.java

image-20230524104931008