Java高并发之深入理解Thread构造函数

80 阅读7分钟

Thread类有多个构造函数,我们可以通过jdk手册查看这些构造方法。没有jdk手册可以从下面的网盘链接中获取。 链接:pan.baidu.com/s/1gV7Q4H_-… 提取码:fw4o

image.png

1 线程命名

每个线程都具有一个名称,这个名称可以在构造Thread实例的时候指定,也可以不指定,由Thread类默认生成。为线程主动赋予一个有意义的名称,可以帮助我们排查问题以及进行线程跟踪。

1.1 默认命名

通过上面的构造函数列表,我们可以看到下面三个构造函数是没有提供线程命名的参数。

  • Thread()
  • Thread(Runnable target)
  • Thread(ThreadGroup group, Runnable target)

通过查看JDK源码可以看出,如果没有指定线程名称,那么该线程的默认名称为"Thread-" + nextThreadNum()。

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

nextThreadNum()是一个自增的整型数字,在JVM进程中不断地增加。

/* For autonumbering anonymous threads. */
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}

可以通过下面的一段代码对其进行验证,在下面的代码中我们循环创建了5个线程,并在每个线程中输出了该线程的名称。

public class ThreadDefaultNaming {
    public static void main(String[] args) {
        IntStream.range(0, 5).boxed().map(i -> new Thread(() -> {
            System.out.println(Thread.currentThread().getName());
        })).forEach(Thread::start);
    }
}

上面代码的运行结果如下:

Thread-0
Thread-1
Thread-3
Thread-2
Thread-4

Process finished with exit code 0

从代码的运行结果可以看出,在不指定线程名称的情况下,JDK默认生成的线程名称确实为"Thread-"加上一个自增的整型,该整型由0开始。

1.2 显式命名

JDK默认生成的线程名称使我们无法很好仅从线程名称来区分不同线程。通过对线程进行显式赋予有意义的名称有助于问题的排查和线程的跟踪。下列构造函数提供了线程命名的参数。

  • Thread(String name)
  • Thread(ThreadGroup group, Runnable target, String name)
  • Thread(ThreadGroup group, Runnable target, String name, long stackSize)
  • Thread(ThreadGroup group, String name)
public class ThreadNaming {
    private static final String PREFIX = "JAVA-";

    public static void main(String[] args) {
        IntStream.range(0, 5).boxed().map(ThreadNaming::createThread).forEach(Thread::start);
    }

    private static Thread createThread(final int intName) {
        return new Thread(() -> {
            System.out.println(Thread.currentThread().getName());
        }, PREFIX + intName);
    }
}

上面的代码和之前默认命名的代码逻辑类似,只是这里我们对线程进行了显式地命名,命名前缀为JAVA-。代码输出如下所示:

JAVA-0
JAVA-4
JAVA-3
JAVA-1
JAVA-2

Process finished with exit code 0

1.3 修改线程的名字

无论是通过JDK设置默认的线程名称还是显式地指定线程名称,在线程开始执行前,我们还有一个修改线程名称的机会,一旦线程启动,则线程名称不能再被修改。线程名称的修改通过Thread类的setName方法执行。setName的源代码如下:

public final synchronized void setName(String name) {
    checkAccess();
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;
    if (threadStatus != 0) {
        setNativeName(name);
    }
}

2 线程的父子关系

通过对Thread构造函数的观察,不难发现每个构造函数都会调用init方法,分析init源码后我们发现新创建的线程会将当前线程作为其父线程。

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;

    Thread parent = currentThread();
    ...
}

由此我们可以得出两个结论:

  • 一个线程的创建肯定是由另外一个线程完成的。
  • 被创建线程的父线程是创建它的线程。

3 Thread与ThreadGroup

在Thread的构造函数中,我们可以显式地指定线程所属地ThreadGroup,如果没有指定,则该线程所属的ThreadGroup为其父线程的ThreadGroup。

SecurityManager security = System.getSecurityManager();
if (g == null) {
    /* Determine if it's an applet or not */

    /* If there is a security manager, ask the security manager
       what to do. */
    if (security != null) {
        g = security.getThreadGroup();
    }

    /* If the security doesn't have a strong opinion of the matter
       use the parent thread group. */
    if (g == null) {
        g = parent.getThreadGroup();
    }
}

然后接下来我们通过一个demo对其进行验证:

public class ThreadConstruction {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1");
        ThreadGroup group = new ThreadGroup("TestGroup");
        Thread t2 = new Thread(group, "t2");
        ThreadGroup mainThreadGroup = Thread.currentThread().getThreadGroup();
        // 输出主线程组的名称
        System.out.println("Main thread belong group: " + mainThreadGroup.getName());
        // 判断t1和主线程是否属于同一个线程组
        System.out.println("t1 and main belong the same group: " + (t1.getThreadGroup() == mainThreadGroup));
        // 判断t2是否输出主线程组
        System.out.println("t2 thread group not belong main group: " + (t2.getThreadGroup() == mainThreadGroup));
        // 判断t2是否属于TestGroup线程组
        System.out.println("t2 thread group belong TestGroup: " + (t2.getThreadGroup() == group));
    }
}

程序的输出结果如下:

Main thread belong group: main
t1 and main belong the same group: true
t2 thread group not belong main group: false
t2 thread group belong TestGroup: true

Process finished with exit code 0

从测试结果我们可以得到如下的结论:

  • main线程所在的线程组名称为main
  • 在未指定线程组的情况下新创建的线程所属的线程组和父线程属于同一个线程组

4 Thread与JVM虚拟机栈

Thread(ThreadGroup group, Runnable target, String name, long stackSize)

在上面的构造函数中,我们发现有一个特殊的参数stackSize,这个参数的作用是什么呢?

4.1 Thread与stackSize

在上面的构造函数的注释文档中对该参数进行了详细的描述:

The stack size is the approximate number of bytes of address space that the virtual machine is to allocate for this thread's stack. The effect of the stackSize parameter, if any, is highly platform dependent.

On some platforms, specifying a higher value for the stackSize parameter may allow a thread to achieve greater recursion depth before throwing a StackOverflowError. Similarly, specifying a lower value may allow a greater number of threads to exist concurrently without throwing an OutOfMemoryError (or other internal error). The details of the relationship between the value of the stackSize parameter and the maximum recursion depth and concurrency level are platform-dependent. On some platforms, the value of the stackSize parameter may have no effect whatsoever.

下面是对上面文档的翻译。

栈大小是虚拟机分配给当前线程的栈地址空间的一个近似的字节数,该参数与平台高度相关。

在某些平台上,指定一个较高的stackSize参数值可能允许线程在抛出StackOverflowError之前到达一个较大的递归深度。同样的,指定一个较小的值可能会允许更多数量的线程同时存在而不抛出OutOfMemoryError。stackSize参数与最大递归深度和并发级别之间关系的细节是平台相关的。在某些平台上,stackSize参数的值可能没有任何影响。 下图展示了stackSize与递归深度之间的关系:

image.png Windows下的10000和100000的测试数据较为奇怪。该参数一般不需要手动进行设置,使用默认值即可。

4 守护线程

守护线程是一类比较特殊的线程,一般用于处理一些后台任务,比如JDK的垃圾回收线程。想要理解守护线程,首先需要了解JVM程序什么情况下退出。下面是JDK文档中的一段话,Java虚拟机只有当所有的线程都是守护线程时才能退出,即不存在非守护线程时Java虚拟机才会退出。

The Java Virtual Machine exits when the only threads running are all daemon threads.

从上面这段话我们可以得到如下的结论:

  • 未结束的用户线程(非守护线程)会阻止JVM进程的退出。
  • JVM进程会在不存在用户线程时退出,这时未结束的守护进程也会退出。 此外:
  • 如果一个线程的父线程为守护线程,那么它的子线程也为守护线程;如果父线程为用户线程,它的子线程也为用户线程。
  • 可以通过线程的setDaemon方法将线程设置为守护线程,需要注意的是setDaemon方法必须在线程启动之前被调用。

JDK的垃圾回收线程是一个典型的守护线程,试想一下,如果垃圾回收线程为用户线程,那么即使main线程完成了工作,JVM进程还是无法退出,因为垃圾回收线程还在工作。