玩转并发-深入理解Thread构造函数
线程的默认命名
下面的几个构造函数,并没有为线程提供命名的参数,那么此时线程会有怎样的命名呢?
Thread()
Thread(Runnable target)
Thread(ThreadGroup group, Runnable target)
打开JDK的源码:
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
如果没有为线程显氏地命名,那么线程会以Thread为前缀与一个自增的数字组合。
线程的父子关系
Thread的所有构造函数都会去调用一个静态方法init。通过查看JDK下的源码,我们发现新创建的任何一个线程都会有一个父线程。
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();
上面代码中的currentThread()是获取当前线程。在执行到此处是,还没有调用start()方法,此时只是一个Thread实例,并不意味着新线程的创建,因此currentThread()代表的是创建它的线程。
- 一个线程的创建肯定是由另一个线程进行的
- 被创建线程的父线程是创建它的线程
Thread与ThreadGroup
JDK源码中关于ThreadGroup:
if (g == null) {
/* 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();
}
}
通过对源码分析,如果在构造Thread时,没有显氏地指定ThreadGroup,那么子线程会加入父线程的ThreadGroup。main所在的线程组叫main
Thread与Runnable
我们查看Thread构造函数的API,发现并不是所有的构造函数都会注入Runnable。
查看JDK源码:
先回顾下上篇文章讲过的start()的源码
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
//start0()是native方法,内部调用了run()
//run的来源可以是Thread的复写,或者是Runnable策略接口的注入
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
//Thread的成员变量
private Runnable target;
//init中的语句
//将参数列表中的target传入给成员变量target
this.target = target;
public void run() {
if (target != null) {
//调用Thread声明的run()
target.run();
}
}
如果在构造Thread中没有传递Runnable或者没有复写run(),该Thread将不会调用任何东西;如果传递了Runnable接口,或者复写了Thread的run()方法,则会执行该方法的逻辑单元。
Thread与JAVA内存结构的关系
JAVA内存结构
JVM内存结构图:
方法区
方法区是多个线程共享的内存区域,主要用于存储已经被虚拟机加载的类信息,常量,静态变量,即时编译器(JIT)编译后的代码等数据。又被称为“非堆”。
堆内存
堆内存是JVM中最大的一块区域,被所有的线程共享。Java在运行期间创建的所有对象几乎都存放在该区域.该内存区域是垃圾回收器重点照顾的对象,因此又被叫作GC堆。
本地方法栈
Java中提供了调用本地方法的接口(Java Native Interface),也就是C/C++程序。JVM为本地方法所划分的内存区域便是本地方法栈。
程序计数器
每个线程都需要有一个独立的程序计数器,方便CPU时间片轮转切换上下文之后能够回到正确原来的位置。JVM将此块内存区域设为线程私有。
虚拟机栈
虚拟机栈也是线程私有的,生命周期与线程相同。在线程中,方法在执行时都会创建一个名叫栈帧的数据结构,主要用于存放变量表,操作栈,动态链接,方法出口等信息。将栈帧的大小称为宽度,栈帧的数量称为虚拟机栈的深度。在构造Thread时,可以指定虚拟机栈的深度。
守护线程
守护线程是一些比较特殊的线程,一般用于处理后台的工作,比如JVM的垃圾回收。在正常情况下,若JVM中没有一个非守护线程,则JVM进程会退出
守护线程作用
如果JVM进程中没有一个非守护线程,那么JVM会退出,也就是说守护线程具备自动结束生命周期的特性。试想下如果JVM的垃圾回收线程不是守护线程,那么当main工作完成了,JVM仍无法退出,因为垃圾回收线程仍在工作。当希望某个线程执行结束,另一线程也结束,可以将后者设置为前者的守护线程(setDaemon)