一、线程中的概念
- 进程:程序运行资源分配的最小单位,进程内部有多个线程,会共享这个进程的资源;
- 线程:CPU 调度的最小单位,在一个 Java 程序进程中,最少有 2 个线程,Main 线程 与 GC 守护线程;
- 上下文切换:CPU 为每个线程分配一个时间段,它称为时间片,如果在时间片结束时线程还在运行,则暂停这个线程的运行,并且 CPU 分配给另一个线程;
- 并行: 同一时刻,可以处理事情的能力,真正意义上的多个任务同时执行;
- 并发:在单位时间内,可以处理事情的能力,多个任务交替执行;
- 线程模型:
- 用户级线程(ULT):用户程序实现,不依赖操作系统核心,应用提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/核心态切换,速度快。内核对 ULT 无感知,线程阻塞则进程阻塞;
- 内核级线程(KLT):系统内核管理线程,内核保存线程的状态和上下文信息,线程阻塞不会引起进程阻塞。在多处理器系统上,多线程在多处理器上并行运行。线程的创建、调度和管理由内核完成,效率比 ULT 要慢,进程操作要快;
Java 线程创建是依赖于系统内核(KLT),通过 JVM 调用系统库创建内核线程,内核线程与 Java 线程是 1:1 的映射关系。
二、Thread 类解析
2.1 线程的状态
线程状态定义位于 Thread.State
枚举类中,定义了如下 6 种状态
- NEW(新建状态):实现 Runnable 或继承 Thread 使用 new 关键字实例对象,但没有调用 start() 方法;
- RUNNABLE(运行状态):由 READY(就绪状态)和 RUNNING (运行中)组成;
- READY(就绪状态):当线程调用 start() 方法,程序进入 READY(就绪状态)等待线程调度器选中执行,还能通过以下方法进入 READY(就绪状态);
- Thread.yield() 当前线程暂时放弃 CPU 抢占权;
- Thread.sleep()、Object.join()、Object.wait() 当前线程挂起,挂起的线程进入等待队列,等待时间结束或被唤醒;
- 当前线程获取到锁;
- RUNNING(运行中):当前线程被线程调度器执行,这也是线程进入该状态的唯一方法;
- READY(就绪状态):当线程调用 start() 方法,程序进入 READY(就绪状态)等待线程调度器选中执行,还能通过以下方法进入 READY(就绪状态);
- TIMED_WAITING(超时等待):线程处于一定时间后自动被唤醒;
- WAITING(等待状态):线程等待被唤醒;
- BLOCKED(阻塞状态):当前线程进入同步代码块,未获得锁对象的线程,进入该锁对象的同步队列,等待抢锁;
- TERMINATED(终止状态):当前线程执行完成;
2.2 优先级与守护线程
优先级代表了线程被线程调度器选中执行的机会大小,优先级高的可能先执行,优先级低的可能后执行。在 Java 中,线程优先级被分为 1 到 10, 默认优先级为 5。在 Thread 类中定义如下,Java 中对线程所设置的优先级只是给操作系统的一个建议,而真正调用顺序,是由操作系统的线程调度算法完成决定的。
/**
* 线程最小优先级
*/
public static final int MIN_PRIORITY = 1;
/**
* 线程默认优先级
*/
public static final int NORM_PRIORITY = 5;
/**
* 线程最大优先级
*/
public static final int MAX_PRIORITY = 10;
守护线程
在创建线程时,默认都是非守护线程,要创建守护线程需要将 daemon
属性设置为 true。守护线程的优先级很低,JVM 在退出时,不会关注有无守护线程,JVM 仍然会退出,一般守护线程用于监控的工作。当线程已经启动时,如果调用 setDaemon
方法将其设置为守护线程,将会抛出 IllegalThreadStateException
异常。在守护线程中创建的线程仍然是守护线程。
2.3 构建一个线程
Thread 类提供了多种构造方法
public Thread() {
this(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
this(null, target, "Thread-" + nextThreadNum(), 0);
}
/**
* 创建一个继承给定 AccessControlContext 的新线程,但不继承线程局部变量
*/
Thread(Runnable target, AccessControlContext acc) {
this(null, target, "Thread-" + nextThreadNum(), 0, acc, false);
}
public Thread(ThreadGroup group, Runnable target) {
this(group, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(String name) {
this(null, null, name, 0);
}
public Thread(ThreadGroup group, String name) {
this(group, null, name, 0);
}
public Thread(Runnable target, String name) {
this(null, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name) {
this(group, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name, long stackSize) {
this(group, target, name, stackSize, null, true);
}
public Thread(ThreadGroup group, Runnable target, String name, long stackSize, boolean inheritThreadLocals) {
this(group, target, name, stackSize, null, inheritThreadLocals);
}
以上构造方法底层均调用一个初始化方法,在 JDK 1.8 中为 init
方法,在 JDK 11 中为私有的 Thread
构造方法,两个 JDK 版本的实现大致相似,以下源码以 JDK 11 为准
/**
* 初始化一个线程
* ThreadGroup g 线程组,线程组可以对组内的线程进行批量操作,比如批量打断 interrupt
* Runnable target 要运行的对象
* String name 线程名称,默认使用 "Thread-" + nextThreadNum(),nextThreadNum 方法返回自增的数字
* long stackSize 设置线程堆栈的大小,如果为 0 表示忽略次参数
* AccessControlContext acc 要继承的 AccessControlContext,如果为 null,则为 AccessController.getContext()
* boolean inheritThreadLocals 是否继承 ThreadLocal 中的初始值
*/
private Thread(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();
/**
* 获取系统安全管理器,安全管理器是一个允许应用程序实现安全策略的类
* 它允许应用程序在执行可能不安全或敏感操作之前确认该操作是什么以及是否允许执行该操作的安全上下文中尝试该操作
*/
SecurityManager security = System.getSecurityManager();
// 设置线程组
if (g == null) {
if (security != null) {
g = security.getThreadGroup();
}
if (g == null) {
g = parent.getThreadGroup();
}
}
// 判断当前线程是否有权限修改该线程组
g.checkAccess();
//
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SecurityConstants.SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
// 增加线程组中未启动线程的计数
g.addUnstarted();
// 子线程继承线程组
this.group = g;
// 子线程继承父线程的守护属性
this.daemon = parent.isDaemon();
// 子线程继承父线程的优先级属性
this.priority = parent.getPriority();
// 继承父线程的 ClassLoader
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
// 是否继承的 AccessControlContext
this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext();
// 设置该线程的执行对象
this.target = target;
// 更改该线程的优先级
setPriority(priority);
// 当 inheritThreadLocals 为 true 或者 父线程的 inheritableThreadLocals 不为空时,会将父线程中所有的 inheritableThreadLocals 传递给子线程
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// 指定线程栈大小
this.stackSize = stackSize;
// 设置线程ID
this.tid = nextThreadID();
}
StackSize
StackSize 参数是指定 JVM 为该线程分配线程栈大小,其参数的效果高度依赖于平台。JVM 可以自由的将 StackSize 视为建议,如果指定值过低,JVM 可能会更改为使用某些特定与平台的最小值;如果指定的值过高,JVM 可能会改为使用某个特定于平台的最大值。
2.4 启动一个线程
实例化一个 Thread 类后,需要调用 start
方法才是真正开启了一个线程,如果只是调用了其 run
方法,只是对其进行了一个方法调用。由于线程的状态是不可逆的,因此一个线程调用 start
方法后,该线程的状态将会变为 RUNNABLE
,如果多次调用 start
方法,该线程将抛出 IllegalThreadStateException
异常。
/**
* 开始执行一个线程
*/
public synchronized void start() {
// 判断线程状态如果不为 NEW ,则抛出异常
if (threadStatus != 0) throw new IllegalThreadStateException();
// 通知线程组该线程即将开启,并将线程组中未启动线程计数递减
group.add(this);
// 线程启动标识
boolean started = false;
try {
// 调用本地方法,真正开启线程执行,并将线程启动标识设置为 ture
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 */
}
}
}
/**
* Java 本地方法
*/
private native void start0();
2.5 线程中常用的方法
- wait 方法,调用该方法的线程进入
WAITING
状态,如果指定了超时时间,该线程进入TIMED_WAITING
状态,并且进入等待此对象的等待锁定池,,只有等待其他线程通知唤醒或中断才会转为RUNNABLE
状态,需要注意 调用 wait() 方法后,会释放对象的锁,因此,wait 方法一般用在同步方法或同步代码块中; - sleep 方法,调用该方法的线程进入
TIMED_WAITING
状态, sleep 方法不会释放当前占有的锁; - join 方法,等待指定线程终止,join 方法的本质上就是调用 wait 方法;
- yield 方法,调用该方法的线程将让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 执行时间片;
- interrupt 方法,中断此线程,并不是真的中断此线程,而是设置标志位(中断位)来通知线程,如果此线程调用了
wait
、join
或sleep
,则该线程的中断状态将被清除并抛出InterruptedException
,实际上只有声明了会抛出 InterruptedException 的函数才会抛出异常,调用 interrupt 方法并不会抛出 InterruptedException;t.isInterrupted 和 Thread.interrupted 的区别在于,前者只用于读取中断状态,后者不仅读取中断状态,还会重置中断标志位; - stop 和 destory 方法都是用来强制关闭线程的,但是官方并不建议使用,因为强制关闭线程,将导致线程中使用的资源,如:文件描述符、网络资源等不能正常关闭。最好的方式应当等待线程执行完成,并完整的释放资源,如果是一个不断循环的线程,应当使用线程之间的通信,让主线程通知其关闭;
三、ThreadGroup 类解析
线程组是线程的集合,其结构类似于树形结构,在树中,除了初始线程组外,其他线程组都有一个父线程组。一个线程可以访问当前线程组信息,但是不能访问其线程组的父级线程组信息或其他线程组信息。
3.1 构建一个线程
线程组提供了两种创建方式。
/**
* 创建指定名称的线程组,其父级线程组为当前线程的线程组
*/
public ThreadGroup(String name) {
this(Thread.currentThread().getThreadGroup(), name);
}
/**
* 创建指定 父级线程组 和 名称 的线程组
*/
public ThreadGroup(ThreadGroup parent, String name) {
this(checkParentAccess(parent), parent, name);
}
以上构造方法均调用线程组私有构造方法,对于一个线程组,它也拥有自己的名字、优先级、是否为守护线程组,并通过 ThreadGroup parent
记录其父级线程组,ThreadGroup groups[]
记录其线程组下的子线程组。
/**
* 创建指定 父级线程组 和 名称 的线程组
*/
private ThreadGroup(Void unused, ThreadGroup parent, String name) {
// 线程组名称
this.name = name;
// 线程组的最大优先级,表示组内所有的线程都不允许超过这个数值
this.maxPriority = parent.maxPriority;
// 线程组是否为守护线程组
this.daemon = parent.daemon;
// 当前线程组的父级线程组
this.parent = parent;
// 将当前线程组加入到 父级线程组中
parent.add(this);
}
另外 ThreadGroup 内部还有个私有的默认构造方法,用于创建系统级线程组。
/**
* 创建一个不在任何线程组中的空线程组,该方法用于创建系统线程组
*/
private ThreadGroup() {
this.name = "system";
this.maxPriority = Thread.MAX_PRIORITY;
this.parent = null;
}
3.2 ThreadGroup 常用的方法
- activeCount 方法,返回此线程组及其子组中活动线程数的估计值,因为该方法内部使用遍历内部数据,线程的数量可能会动态发生变化,因此该方法返回的是估计值;
- activeGroupCount 方法,返回此线程组及其子组中活动组的数目的估计值;
- checkAccess 方法,判断当前运行的线程是否有权限修改当前线程组,如果不能修改则抛出
SecurityException
异常; - destroy 方法,销毁当前线程组及其子组,调用该方法时,需要确保当前线程组为空,且当前线程组中的线程全部完成。如果线程组不为空或线程组已被销毁,将抛出
IllegalThreadStateException
异常; - enumerate 方法,将此线程组及其子组中的每个活动线程复制到指定的数组中,如果 recurse 为 ture ,则此方法递归枚举此线程组的所有子组,并且还包括对这些子组中每个活动线程的应用;
- interrupt 方法,中断此线程组及其子组中的所有线程;
- list 方法,将有关此线程组的信息打印到标准输出;
- parentOf 方法,判断当前线程组是否为传入线程组的祖先级线程组;