Java 线程池源码解读与实践
肖文英
Java 资深研发工程师
27 人已学习
立即订阅
课程目录
已更新 28 讲/共 32 讲
并发编程基础知识 (8讲)
时长 09:08
时长 40:33
时长 21:26
时长 44:37
时长 09:11
线程池基础知识 (6讲)
时长 12:38
时长 09:28
时长 30:59
时长 44:51
时长 45:02
时长 15:15
线程池实现详解 (8讲)
时长 14:10
时长 23:21
时长 25:00
时长 13:57
时长 36:02
时长 17:28
时长 42:08
常见开源线程池 (3讲)
时长 22:14
时长 27:06
时长 25:32
Java 线程池源码解读与实践
15
15
1.0x
00:00/00:00
登录|注册

中断线程

1 线程阻塞

线程阻塞通常是指一个线程在执行过程中暂停,以等待某个条件的触发。而什么情况才会使得线程进入阻塞的状态呢?
等待阻塞:运行的线程执行 wait() 方法,该线程会释放占用的所有资源,JVM 会把该线程放入“等待池”中。进入这个等待状态 (WAITING) 后,是不能自动唤醒的,必须依靠其它线程调用 notify() 或 notifyAll() 方法才能被唤醒,或者是被中断后才能唤醒,然后被调度后开始运行或同步阻塞。
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁 (synchronized) 被别的线程占用,则 JVM 会把该线程放入“锁池”中,注意这种阻塞 (BLOCKED) 是不能被打断的。
还有运行的线程执行 sleep() 或 join() 方法,或者发出了可以中断的 I/O 请求时,JVM 会把该线程置为等待 (WAITING) 状态:当 sleep() 状态超时、join() 等待线程终止或者超时、或者可以中断的 I/O 处理完毕时,线程重新转入运行状态 (被调度后开始运行或等待调度);此外线程被中断后也能进入运行状态。
在 Java 中,最好的停止线程的方式是使用中断 interrupt,但是这仅仅是会通知到被终止的线程“你该停止运行了”,被终止的线程自身拥有决定权(决定是否、以及何时停止),这依赖于请求停止方和被停止方都遵守一种约定好的编码规范。任务和线程的启动很容易。在大多数时候,我们都会让它们运行直到结束。然而有时候我们希望提前结束任务或线程(或许是因为用户取消了操作或者服务需要被快速关闭,或者是运行超时或出错了)。
要使任务和线程能安全、快速、可靠地停止下来,并不是一件容易的事。Java 没有提供任何机制来安全地终止线程;但它提供了中断,这是一种协作机制,使用得当时能够使一个线程终止另—个线程的当前工作。
这种协作式的方法是必要的,我们很少希望某个任务、线程或服务立即停止,因为这种立即停止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务时可以使用种协作的方式,当需要停止时它们首先会清除当前正在执行的工作然后再结束。
这提供了更好的灵活性,因为任务本身的代码比发出取消请求的代码更清楚如何执行清除工作。生命周期结束的问题会使任务、服务以及程序的设计和实现等过程变得复杂,而这个在程序设计中是非常重要的要素却经常被忽略。一个在行为良好的软件与勉强运行的软件之间最主要区别就是,行为良好的软件能很完善地处理失败、关闭和取消等过程。

2 响应中断异常的常见方法

2.1 Object 各种 wait 方法

wait() 
wait(long timeout, int nanos)  
wait(long timeout)

2.2 lock.Condition 全部 await 方法

await() 
awaitNanos(long nanosTimeout) 
await(long time, TimeUnit unit) 
awaitUntil(Date deadline)

2.3 Lock 部分加锁方法

只有 lockInterruptibly() 响应中断,下面其他加锁方法都不响应中断:
lock() 
tryLock()
tryLock(long time, TimeUnit unit)

2.4 Thread 部分方法

sleep(long) 静态方法 
sleep(long, int) 静态方法 
join(long) 实例方法
join(long, int) 实例方法
join() 实例方法

3 中断相关方法

3.1 thread.interrupt()

该方法用于中断 Thread 线程,此线程并非当前线程,不是强制关闭线程,而是传递给目标线程一个“应该关闭”的信号,然后由目标线程自行处理。“应该关闭”的信号的具体体现:
当目标线程处于阻塞状态,会抛出 InterruptedException,而不会将中断标识位设置为 true。
当目标线程处于非阻塞状态,会将中断标识位设置为 true,不会抛出异常。

3.2 Thread.interrupted()

该方法是静态方法,调用时会重置当前线程的中断状态,如果线程被中断了,调用这个方法第一次会返回 true,调用第 n 次 (n>=2) 会返回 false;如果线程没有被中断,那么调用这个方法会返回 false,调用第 n 次 (n>=2) 会返回 false。

3.3 thread.isInterrupted()

该方法是实例方法,不会重置对应线程的中断状态,如果线程被中断了返回 true,否则返回 false。
public static class MyThred extends Thread {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("Someone interrupted me.");
} else {
System.out.println("Thread is Going...");
}
}
}
}
public static void main(String[] args) throws InterruptedException {
MyThred t = new MyThred();
t.start(); //输出Thread is Going...
Thread.sleep(3000); //3秒后 开始输出Someone interrupted me.
t.interrupt();
}

4 中断场景分析

如果我们有一个运行中的软件,例如是杀毒软件正在全盘查杀病毒,此时我们想让它停止杀毒,这时候点击取消,那么就是正在中断一个运行的线程。
每一个线程都有一个 boolean 类型的实例变量,此变量表示当前的线程是否收到了中断请求,默认为 false。当一个线程 A 调用了线程 B 的 interrupt 方法时,那么线程 B 的中断标识变为 true。而线程 B 可以调用某些方法检测到此标识的变化。
阻塞方法:如果线程 B 调用了能响应中断的阻塞方法,并且在阻断过程中或被阻断前中断标识变为了 true,那么它会抛出 InterruptedException 异常。抛出异常的同时它会将线程 B 的中断标识置为 false。
非阻塞方法:可以通过线程 B 的 isInterrupted 方法进行检测中断标识为 true 还是 false。另外还有一个静态的方法 interrupted 方法也可以检测标志,但是该静态方法检测完以后会自动将是否请求中断标识位置为 false。
下面为具体的例子:

4.1 等待时被中断

public class InterrupTest implements Runnable{
public void run(){
try {
while (true) {
Boolean flag = Thread.currentThread().isInterrupted();
System.out.println("isInterrupted=" + a);
Thread.sleep(20000);
System.out.println("sleep is over");
}
} catch (InterruptedException e) {
// 如果不加上这一句,那么c和d将会都是false,
// 因为在捕捉到InterruptedException异常的时候就会把中断标志置为false
Thread.currentThread().interrupt();
Boolean first=Thread.interrupted();
Boolean second=Thread.interrupted();
System.out.println("first="+first);
System.out.println("second="+second);
}
}
public static void main(String[] args) {
InterrupTest si = new InterrupTest();
Thread t = new Thread(si);
t.start();
//主线程休眠2秒,从而确保刚才启动的线程有机会执行一段时间
try {
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
//中断线程t
t.interrupt();
System.out.println("main has interrupted other thread");
System.out.println("main return");
}
}
打印的参数如下:
isInterrupted=false
main has interrupted other thread
main return
first=true
second=false
现在知道线程可以检测到自身的标志位的变化,但是它只是一个标志,如果线程本身不处理的话,那么程序还是会执行下去。
因此 interrupt 方法并不能立即中断线程,该方法仅仅告诉线程外部已经有中断请求,至于是否中断还取决于线程自己。

4.2 等待前已经被中断

public class InterrupTest implements Runnable {
public void run() {
try {
System.out.println("sleep for 20 seconds before, time="+ LocalTime.now());
Thread.sleep(20000);
System.out.println("sleep return, time="+ LocalTime.now());
} catch (InterruptedException e) {
System.out.println("it is interrupted while in sleep, time="+ LocalTime.now());
System.out.println("it is interrupted while in sleep,interrupted status = " + Thread.currentThread().isInterrupted());
Thread.currentThread().interrupt();
System.out.println("it is interrupted while in sleep,set to true , interrupted status = " + Thread.currentThread().isInterrupted());
}
System.out.println("run again, time="+ LocalTime.now());
try {
// sleep开始前线程已经被中断
Thread.sleep(20000);
} catch (InterruptedException e) {
System.out.println("it is interrupted again while in sleep, time="+ LocalTime.now());
}
System.out.println("run again,time="+ LocalTime.now());
}
public static void main(String[] args) {
InterrupTest si = new InterrupTest();
Thread t = new Thread(si);
t.start();
//主线程休眠2秒,从而确保刚才启动的线程有机会执行一段时间
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中断线程t
t.interrupt();
System.out.println("main has interrupted other thread");
System.out.println("main return");
}
}
打印日志如下
sleep for 20 seconds before, time=09:01:38.022
main has interrupted other thread
main return
it is interrupted while in sleep,time=09:01:39.800
it is interrupted while in sleep,interrupted status = false
it is interrupted while in sleepset to true , interrupted status = true
run again, time=09:01:39.800
it is interrupted again while in sleep, time=09:01:39.800
run again, time=09:01:39.800

4.3 线程开始运行前已经被中断

如果线程在开始运行前被中断不会把它的中断标识设置为 true,那么线程开始运行后是不会收到中断信号。
public class InterrupTest implements Runnable {
public void run() {
try {
System.out.println("sleep for 20 seconds before, time="+ LocalTime.now());
Thread.sleep(20000);
System.out.println("sleep return, time="+ LocalTime.now());
} catch (InterruptedException e) {
System.out.println("it is interrupted while in sleep, time="+ LocalTime.now());
System.out.println("it is interrupted while in sleep,interrupted status = " + Thread.currentThread().isInterrupted());
Thread.currentThread().interrupt();
System.out.println("it is interrupted while in sleep,set to true , interrupted status = " + Thread.currentThread().isInterrupted());
}
System.out.println("run again, time="+ LocalTime.now());
}
public static void main(String[] args) {
InterrupTest si = new InterrupTest();
Thread t = new Thread(si);
t.interrupt();
System.out.println("main has interrupted other thread before it start");
t.start();
System.out.println("main has started other thread");
System.out.println("main return");
}
}
打印日志如下
main has interrupted other thread before it start
main has started other thread
main return
sleep for 20 seconds before, time=09:06:05.552
run again, time=, time=09:06:25.556

5 InterruptedException 如何处理

通常线程会在什么情况下正常结束:运行完 run 方法就会正常停止线程,但是在运行任务过程中如何发生了中断,我们需要根据具体业务需求,来处理中断,可以分为以下场景。

5.1 中断后线程进入终止状态

通常我们会使用生产 - 消费者模式处理任务,而处理任务的逻辑是在一个 while 死循环中不断的获取然后处理任务;但是我们想把这个线程终止,那么可以对它执行 interrupt 方法,那么这个线程会在 take 等待时跳出 while 循环,并且在执行完 catch 语句后终止,线程一旦终止是不能重新运行的,这样我们也不用关心这个线程是否被中断过了。
public class TaskRunner implements Runnable {
private BlockingQueue<Task> queue;
public TaskRunner(BlockingQueue<Task> queue) {
this.queue = queue;
}
public void run() {
try {
while (true) {
Task task = queue.take(8, TimeUnit.SECONDS);
task.execute();
}
}
catch (InterruptedException e) {
Log.info("worker thread is terminated");
}
}
......
}

5.2 被中断后需要做业务处理

如果线程被中断后没有立刻停止,还需处理其它业务逻辑,那么我们需要把中断标识保留并传递给后续的业务处理逻辑。
下面是线程池运行任务的部分代码:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// getTask 进入等待状态可能会被中断
while (task != null || (task = getTask()) != null) {
w.lock();
// 如果当前线程被中断了,那么Thread.interrupted()会返回true,
// 并且会重置中断标识为false
if ((runStateAtLeast(ctl.get(), STOP) || //条件1-1
(Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) //条件1-2
&& !wt.isInterrupted()) //条件2
// 中断当前线程:保留中断标识,用于后续的业务逻辑处理(run方法)
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 在run方法中可能存在能响应中断的方法,对应getTask等待前已经被中断的情况
task.run();
}
// 能响应中断的方法抛出InterruptedException,进入catch语句块执行
catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}// while
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}

5.3 在 while 外围使用 try/catch

如果在执行过程中,每次循环都会调用 sleep 或 wait 等方法,那么不需要每次迭代都检查是否已中断,sleep 或 wait 等方法会响应中断
public class Test {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
while (num <= 10000) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
Thread.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
打印结果:
0100的倍数
100100的倍数
200100的倍数
300100的倍数
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.hd.thread.stop.RightWayStopThreadWithSleepEveryLoop.lambda$main$0(RightWayStopThreadWithSleepEveryLoop.java:20)
at java.lang.Thread.run(Thread.java:745)
这个案例中我们不再需要每次检查是否中断,sleep() 阻塞时候会自动检测中断,即不再需要:Thread.currentThread().isInterrupted(),我们只需要用 try-catch 包围 while 循环即可,这样发生中断异常时也能跳出 while 循环。但是这样做有一个问题,如果我们在 sleep 结束后 num++ 执行前对线程发出中断,那么最终会只能 num++ 后在执行 sleep 时才能响应中断,如果业务不允许这种情况发生,那么我们需要调整代码在 num++ 操作执行前判断是否当前线程被中断了。

5.4 while 内 try/catch 的问题

如果 while 里面放 try/catch,会导致中断失效。所以我们需要在捕获中断异常后设置中断标识为 true,这样在后续处理时才能识别到中断请求
public class Test {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
while (num <= 10000 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
// 应该调用Thread.curentThread.interrupt()方法
}
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
打印结果:
0100的倍数
100100的倍数
200100的倍数
300100的倍数
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.hd.thread.stop.CantInterrupt.lambda$main$0(CantInterrupt.java:20)
at java.lang.Thread.run(Thread.java:745)
400100的倍数
500100的倍数
600100的倍数
...
Thread.sleep 检测到的中断信号抛出的异常被捕获,这样当前线程的中断标被重置为 false,所以 while 循环没有停止。

6 最佳实践

最佳实践:catch 了 InterruptedExcetion 之后的优先选择:在方法签名中抛出中断异常。对于下面代码,我们就可以在 run() 内使用 try/catch 捕获异常 (编译器会提示需要捕获中断异常)
public class Test implements Runnable {
@Override
public void run() {
while (true && !Thread.currentThread().isInterrupted()) {
System.out.println("start");
try {
throwInMethod();
} catch (InterruptedException e) {
// 设置中断标识为true,确保没有丢失中断信号
Thread.currentThread().interrupt();
//保存日志、停止程序
System.out.println("处理中断异常");
e.printStackTrace();
}
}
}
// 在方法签名中抛出中断异常
private void throwInMethod() throws InterruptedException {
Thread.sleep(2000);
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
打印结果:
start
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.xwy.Test.throwInMethod(Test.java:27)
at com.xwy.Test.run(Test.java:16)
at java.lang.Thread.run(Thread.java:745)
处理中断异常
处理中断的最好方法是什么?
优先选择在方法上抛出异常。用 throws Interrupted Exception 标记你的方法,而不是用 try 语句块捕获异常,这样以便于该异常可以传递到顶层,让 run 方法可以捕获这一异常,例如
private void throwInMethod() throws InterruptedException {
Thread.sleep(2000);
}
由于 run 方法内无法抛出 checked Exception(只能用 try catch),顶层方法必须处理该异常,避免了漏掉或者被吞掉的情况,增强了代码的健壮性。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
  • 解释
  • 总结
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 线程池源码解读与实践》
立即购买
登录 后留言

精选留言

由作者筛选后的优质留言将会公开显示,欢迎踊跃留言。
收起评论
大纲
固定大纲
1 线程阻塞
2 响应中断异常的常见方法
2.1 Object 各种 wait 方法
2.2 lock.Condition 全部 await 方法
2.3 Lock 部分加锁方法
2.4 Thread 部分方法
3 中断相关方法
3.1 thread.interrupt()
3.2 Thread.interrupted()
3.3 thread.isInterrupted()
4 中断场景分析
4.1 等待时被中断
4.2 等待前已经被中断
4.3 线程开始运行前已经被中断
5 InterruptedException 如何处理
5.1 中断后线程进入终止状态
5.2 被中断后需要做业务处理
5.3 在 while 外围使用 try/catch
5.4 while 内 try/catch 的问题
6 最佳实践
显示
设置
留言
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部
文章页面操作
MAC
windows
作用
esc
esc
退出沉浸式阅读
shift + f
f11
进入/退出沉浸式
command + ⬆️
home
滚动到页面顶部
command + ⬇️
end
滚动到页面底部
⬅️ (仅针对订阅)
⬅️ (仅针对订阅)
上一篇
➡️ (仅针对订阅)
➡️ (仅针对订阅)
下一篇
command + j
page up
向下滚动一屏
command + k
page down
向上滚动一屏
p
p
音频播放/暂停
j
j
向下滚动一点
k
k
向上滚动一点
空格
空格
向下滚动一屏
播放器操作
MAC
windows
作用
esc
esc
退出全屏
⬅️
⬅️
快退
➡️
➡️
快进
空格
空格
视频播放/暂停(视频全屏时生效)