【Java 多线程】5 - 2 多线程常用方法
2§5-2 多线程常用方法
5-2.1 Thread
提供的方法
Thread
提供了许多多线程常用的方法。
成员方法:
方法 | 描述 |
---|---|
String getName() |
返回线程名称 |
void setName(String name) |
设置线程名称 |
void start() |
启动线程执行,由 JVM 执行线程的 run 方法 |
静态方法:
静态方法 | 描述 |
---|---|
Thread currentThread() |
返回当前执行线程对象的引用 |
void sleep(long millis) |
设置线程休眠时间(毫秒) |
注意:
-
start
方法会让两条线程并发运行:一条是当前线程(由start
方法返回),另一条则是执行run
方法的线程; -
start
不可在一条线程上调用多次,即线程仅能够启动一次,二次启动线程会抛出异常IllegalThreadStateException
; -
线程 ID 在线程创建时生成,是一个正长整型。每个线程 ID 是唯一的,在线程生命周期内保持不变。线程结束时,其 ID 可能被重用;
-
若未指定线程名称,其默认值为
Thread-X
,序号X
默认从 0 开始; -
线程名称可以通过
setName
方法设置,也可以通过构造器设置(Thread
),若要在子类构造器中实现,子类构造器需要重载; -
JVM 启动时,会自动启动多条线程,其中一条线程就是
main
线程,其作用就是调用main
方法,并执行其中的代码;在以前,所写的所有代码都是单线程运行在
main
线程中; -
调用
sleep
方法的线程会休眠指定时间,时间到了之后,线程会自动唤醒,继续执行; -
Java 程序启动时,JVM 会启动一条名为
main
的线程,运行main
方法;每一条线程都有一个自己的栈,称为线程栈;
5-2.2 线程优先级
与线程优先级有关的成员方法有:
成员方法 | 描述 |
---|---|
int getPriority() |
返回线程优先级 |
void setPriority(int newPriority) |
设置线程优先级 |
在了解线程优先级前,先来了解线程的调度机制。线程调度机制有两种:
- 抢占式调度:多条线程抢占 CPU 的执行权,任何一条线程的执行时机和执行时间都是随机的;
- 非抢占式调度:多条线程按照顺序轮流由 CPU 执行,每条线程的执行时间差不多;
Java 采取抢占式调度的线程调度方式,通过线程优先级调整每条线程抢占到 CPU 的执行权的概率,线程的执行随机。
Thread
类定义了三个与线程优先级相关的常量:
public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;
这三个常量分别定义了线程最小优先级、默认优先级、最大优先级。不同的优先级只表明线程抢占到 CPU 执行权的概率,更高的优先级并不能说明线程一定能够优先执行完毕。
5-2.3 线程的生命周期和状态
与线程状态有关的方法有:
方法 | 描述 |
---|---|
Thread.State getState() |
返回线程的状态 |
线程的状态定义在了 Thread
的内部枚举类中。
下图简要地说明了线程的生命周期:
实际上,Java 在 Thread
内部定义了一个枚举类 State
,用于声明线程的状态。
该类中有六个枚举常量,定义了线程的六种状态,线程在任意时刻只能为这六种状态中的其中之一:
枚举常量 | 描述 |
---|---|
NEW |
使用 new 运算符创建的线程,尚未开始,代码尚未执行。 |
RUNNABLE |
调用了 start 方法后的线程,线程可能正在执行,也可能并不正在执行。 |
BLOCKED |
线程阻塞,等待内置对象锁(并非 java.util.concurrent.Lock )。在其他所有线程释放锁且调度器允许该线程持有锁时,线程会被取消阻塞。 |
WAITING |
无限期等待另一个线程通知调度器某一个条件时,线程会进入等待状态 |
TIMED_WAITING |
调用具有超时参数的方法,线程进入计时等待状态 |
TERMINATED |
线程终止(死亡) |
注意:
-
RUNNABLE
状态:线程在执行时,并不是一直都处于执行状态,而是由操作系统提供调度服务,给予线程一定时间来运行。所有现代桌面和服务器操作系统采用的都是抢占式调度系统。抢占式调度系统会给每一条线程一个时间片执行其任务,当时间片耗尽时,操作系统会再次采用抢占式调度让其他线程有机会工作。选择线程时,操作系统会考虑现成的优先级;也有少部分设备(如手机)可能会采用合作式调度策略,这种情况下,当且仅当线程调用了
yield
方法、被阻塞或等待时才会失去对 CPU 的控制; -
BLOCKED
和WAITING
状态:当线程被阻塞或处于等待状态时,线程会暂时处于非活跃状态,不执行任何代码,消耗最小的资源。由线程调度器重新激活该线程;有关线程等待的内容会在[5 - 4 等待唤醒机制](5 - 4 等待唤醒机制.md)一节中说明。由于调用
Object.wait
,Thread.join
方法、等待Lock
或Condition
,线程会进入等待状态。实际上,阻塞和等待状态的区别并不是特别明显;有些方法具有超时参数。调用这些方法会使得线程进入
TIMED_WAITING
计时等待状态。该状态会一直持续,直至超时,或收到正确通知时退出该状态。具有超时参数的方法有Thread.sleep
,以及超时版本的Object.wait
,Thread.join
,Lock.tryLock
和Condition.await
; -
TERMINATED
状态:线程进入终止状态主要有两种原因。run
方法正常退出,线程自然死亡;- 未捕获的异常终止了
run
方法,线程突然终止;
考虑到可能存在第二种情况,为提高程序的健壮性,应当适当地处理可能抛出的异常。
5-2.4 礼让和插入线程
与礼让和插入线程有关的方法有:
方法 | 描述 |
---|---|
void join() void join(long millis) void join(long millis, int nanos) |
等待线程终止 |
static void yeild() |
提示调度器出让当前线程对某个处理器的使用 |
注意:
-
每条线程都会抢占 CPU 的执行权,得到执行权的线程会让 CPU 执行该线程一段随机长度时间;
-
调用
yeild
方法,会无视其可能继续执行的时间,出让线程对该处理器的执行权,让线程重新抢夺执行权; -
调用
yeild
方法会尽可能地使多条线程的执行时间平均化,但同一条线程仍有可能二次抢夺到执行权;平均化的前提是多条线程都有在出让线程;
-
join
方法用于将指定线程插入到当前线程前,优先执行完所插入线程后再继续执行当前线程;所插入的线程应当是已经启动的线程;
5-2.5 中断线程
与中断线程有关的方法有:
方法 | 描述 |
---|---|
void interrupt() |
中断该线程 |
static boolean interrupted() |
测试当前线程是否已经被中断,会清除线程的中断状态 |
boolean isInterrupted() |
测试该线程是否已经被中断 |
废弃的老旧方法:在最初的 Java 版本中,stop
, suspend
, resume
方法可用于终止/挂起/继续一条线程。但是,自 JDK 1.2 起,这些方法就被废弃了。因此,在当前版本中,除了这些方法,并不存在一个方法能够强制终止一条线程。在实际开发过程中也不推荐使用这些方法。
中断线程的使用方法:但某些情况下,我们希望线程能够终止其运行,这时就需要使用 interrupt
中断线程。这个方法并不会立刻中断线程,而是会将线程的中断状态(interrupted status,一个 boolean
标志位)设置为 true
。每条线程都具有这样的一个标志位,线程应当不时检查该标志位以确定自己是否被请求中断。
// 检查线程是否被中断
while (!Thread.currentThread().isInterrupted() && ...) {
...
}
但是,若被中断的线程正处于阻塞或等待状态时,尝试中断则会抛出 InterruptedException
异常。这是一个受查异常,由于 run
方法并没有声明抛出任何异常,因此只能够通过 try-catch-finally
环绕处理。有些线程所执行的任务十分重要,这些线程在处理中断请求所抛出的异常时应当格外小心。但更常见的情况是,大部分线程被请求中断都是为了终止该线程。
// 捕获可能发生的 InterruptedException
Runnable r = () -> {
try {
while (!Thread.currentThread().isInterrupted() && ...) {
...
}
} catch (InterruptedException e) {
// 异常处理
...
} finally {
// 善后工作
...
}
}
如果在每一轮循环迭代后,都会调用一个 sleep
或其他可中断方法,那么调用 isInterrupted
的检查就不是很有必要,也不好用。因为该线程进入阻塞状态后,其他线程可能会通过该线程对象向它发出中断请求,被中断线程就会立刻收到一个 InterruptedException
。在这种情况下(循环体中调用阻塞方法),直接捕获异常比检查中断状态更好:
// 循环体调用阻塞方法,直接捕获异常
Runnable r = () -> {
try {
while (...) {
...
Thread.sleep(delay);
}
} catch (InterruptedException e) {
// 异常处理
...
} finally {
// 善后工作
...
}
}
中断异常处理:异常处理是程序中十分重要的一个部分。在 catch
语句块中,应当妥善处理可能抛出的异常,千万不要什么也不做。若实在不知道该怎么做,有两种选择:
-
在
catch
语句块中,调用Thread.currentThread().interrupt()
以设置中断状态,使得调用者能够测试中断状态;void subTask() { ... try { Thread.sleep(delay); } catch (InterruptedExcepion e) { // 异常处理 Thread.currentThread().interrupt(); } finally { // 善后工作 ... } }
这个方法由
run
方法或其子方法调用。 -
或者,在方法签名处附上
throws InterruptedException
,去掉try
语句块,让调用者(或,最终调用者为run
方法)能够捕获该异常;void subTask() throws InterruptedException { ... Thread.sleep(delay); ... }
5-2.6 守护线程
与守护线程相关的方法有:
成员方法 | 描述 |
---|---|
boolean isDaemon() |
测试线程是否为守护线程 |
void setDaemon(boolean on) |
将线程标记为守护线程或用户线程 |
注意:
- 当所有正在运行的线程都为守护线程时,JVM 会退出;
setDaemon
方法必须在线程启动前调用,否则抛出异常IllegalThreadStateException
;- 若
on
为true
时,标志着该线程为守护线程;
守护线程的特点:当所有非守护线程执行完毕而退出后,守护线程会陆续退出。
注意,守护线程在此时会陆续退出,而不是立刻退出。
应用场景:一个应用程序往往会在后台创建多条线程,但一定会有一条线程为主线程,与程序主界面有关。当这条主线程退出时,其他线程可没有继续执行下去的必要了,就可以将这些线程设置为守护线程。一旦主线程退出,这些守护线程就没有继续执行下去的必要,陆陆续续退出。
例如:一个聊天软件,一条线程负责聊天,一条负责文件传输。当聊天线程结束时,文件传输没有继续执行的必要了,则可设置为守护线程。
5-2.7 处理未捕获的异常
当线程由于未捕获的异常而终止运行时,虚拟机则会调用接口 UncaughtExceptionHandler
(Thread
的内部接口)处理这个异常,将终止线程与异常作为参数传递给该接口。
与未捕获异常处理器有关的方法:
静态方法 | 描述 |
---|---|
Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() |
返回线程由于未捕获的异常意外终止时调用的默认处理器 |
void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler ueh) |
设置线程由于为捕获的异常意外终止时调用的默认处理器 |
实例方法 | 描述 |
---|---|
Thread.uncaughtExceptionHandler getUncaughtExceptionHandler() |
返回线程由于未捕获的异常意外终止时调用的处理器 |
void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler ueh) |
设置线程由于未捕获的异常意外终止时调用的处理器 |
UncaughtExcepionHandler
接口:这是一个函数式接口,有且仅有一个方法。
方法 | 描述 |
---|---|
void uncaughtException(Thread t, Throwable e) |
当所给的线程由于未捕获异常而终止时调用,该方法抛出的任何异常都会被虚拟机忽略 |
注意:
-
线程由于未捕获异常而终止时,虚拟机调用
Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread, Throwable)
处理异常(若有自定义处理器,则getUncaughtExceptionHandler().uncaughtException(Thread, Throwable)
); -
可以通过方法
setDefaultUncaughtExceptionHandler
或setUncaughtExceptionHandler
为任意一条线程单独设置(默认)未捕获异常处理器。注意,传递的参数应当为该接口的实现类,替代的处理器可以使用日志 API 将未捕获的异常报告发送到一个日志文件中; -
若没有设置默认处理器,则默认处理器为
null
。若没有为一条线程设置异常处理器,则处理器为线程的ThreadGroup
对象;ThreadGroup
简介:表示一组线程的线程组,实现了UncaughtExceptoinHandler
接口,提供了一种管理线程的方式。默认情况下,手动创建的所有线程都处于同一线程组,也允许手动创建新的线程组。由于在线程集合上操作能实现更好的特性,因此不推荐在程序中使用线程组。ThreadGroup
的uncaughtException
方法行为如下:- 若该线程组具有父级线程组,则将相同参数传入父级线程组的
uncaughtException
方法并调用之; - 否则,方法检查线程是否具有默认异常处理器。若有,则传入相同参数,调用该处理器处理异常;
- 否则,包含线程名称(
getName
)、线程栈回溯的错误信息将会通过Throwable
的printStackTrace
方法打印到标准错误流(System.err
)中;
- 若该线程组具有父级线程组,则将相同参数传入父级线程组的