领导说谁再用Stop直接下岗,这样终止线程更优雅

开发 前端
即使我们先调用了unpark,再调用park也是可以正常唤醒线程的,因为unpark获取了一个许可证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

本文收录于《Java并发编程》合集,本文主要介绍Java并发编程中终止线程的手段,通过本文您可以了解到:

  • 通过Thread类提供的方法中断线程
  • 中断线程的应用场景和代码实现,以及实现中的细节处理
  • stop方法中断线程存在的隐患
  • LockSupport停止和唤醒线程
  • LockSupport工具类的park和unpark的原理

原本的Java线程Thread类API中提供了stop这样的终止线程的方法,但是已被标记为过时方法,此方法来终止线程是暴力的不安全的,没有对线程做后续的善后操作而直接终止,往往会埋下一些隐患。我们可以通过Java线程的中断机制,来安全的停止线程。

Java提供了线程的中断机制:设置线程的中断标志,可以使用它来决定是否结束一个线程。通过设置线程的中断标志并不能直接终止线程,这种机制其实就是告知线程我希望打断你,至于到底停止不停止是由线程决定。

打断线程场景

比如打断或者重新执行一些耗时过长任务,多线程同时完成同一个相同任务,某一线程如果执行完就通知其他线程可以停止。比如:

  • 下载任务,发现需要耗时过长,可以直接取消下载,其实就是打断这个下载数据线程
  • 比如抢票软件,开启多个线程抢多个车次的车票,如果某一个线程抢到,就通知其他线程可以终止
  • 比如服务器中的超时操作,长时间没有获取到数据,就终止线程

你在哪里使用过打断线程呢?

Thread类相关API

  • void interrupt():中断线程,例如线程A运行时,线程B调用线程A的interrupt方法来设置线程A的中断标志为true。注意:这里仅仅是设置了标志,线程A并没有中断,它会继续往下执行。如果线程A调用了wait,join,sleep方法而被阻塞挂起,如果此时调用线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回。
  • boolean isInterrupted():检测当前线程是否被中断,如果是返回true,否则返回false。
  • boolean interrupted():检测当前线程是否被中断,这个方法是Thread类的静态方法;与interrupt()方法的区别在于,如果发现当前线程被中断,则会清除中断标志。

另外需要注意的是:interrupted()方法是获取当前调用线程【正在运行的线程】的中断标志,而不是调用interrupted()方法的线程的中断标志。

打断线程

public class InterruptThread {

public static void main(String[] args) throws InterruptedException {

// 开启线程
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "运行......");
// 线程进入睡眠状态
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1线程");
// 启动 t1 线程
t1.start();
// 等待1秒后打断t1线程
Thread.sleep(1000);
System.out.println("打断......");
// 打断线程
t1.interrupt();
// 查看打断标记
System.out.println("打断标记:" + t1.isInterrupted());
}
}

运行结果:

  • 调用 interrupted方法之后如果被打断的线程【t1】线程中调用sleep、wait、join方法会触发异常
  • 调用isInterrupted方法获取打断标记,true为打断,false为未打断

线程中调用wait方法

public class InterruptThread {

public static void main(String[] args) throws InterruptedException {

// 创建线程1
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "运行......");
},"t1线程");

// 创建线程2
Thread t2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "运行......");
// 调用wait方法
try {
t1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2线程");
// 启动线程
t1.start();
t2.start();
// t2线程插队执行
t2.join();
// 打断t2线程
t2.interrupt();
// 查看t2线程打断标记
System.out.println("t2打断标记:" + t2.isInterrupted());
}
}

运行结果:

  • 调用wait方法之后仍然会触发异常
  • 打断标记被重置变为false,所以可以在catch块中设置打断标记为true

终止线程

此时可以根据打断标记,在线程内部判断是否需要打断

案例:小明放假回家,妈妈想着给小明做好吃的,但是小明失恋了没有胃口,就给妈妈说不要做了,妈妈收到打断消息之后就停止做饭。

public class KitChenThread {
public static void main(String[] args) throws InterruptedException {

// 创建妈妈线程
Thread t1 = new Thread(() -> {
// 整东西吃
System.out.println("儿子放假了,整个烩面!吭哧吭哧~~~");
while (true) {
// 获取当前线程打断状态
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
System.out.println("whis today,饿你轻!");
// 停止
break;
}
}
},"妈妈线程:");

// 创建小明线程
Thread t2 = new Thread(() -> {
System.out.println("妈,我不想吃,你别弄了......");
// 打断妈妈线程
t1.interrupt();
},"小明线程:");

// 启动线程
t1.start();
t2.start();
// 控制执行顺序
t1.join();
t2.join();
}
}

运行结果:妈妈线程做饭,当小明线程打断之后,妈妈线程就收到打断信息,停止运行

通过代码我们发现,其实线程的终止权在被打断的线程中,通过判断打断标记来控制是否终止,也就是小明不让妈妈做饭,妈妈可以不做也可以继续做。

停止线程其他方法

  • 使用Thread类中的stop()方法
  1. 调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如关闭文件数据流,关闭数据库连接等。
  2. 调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。
  • 使用System.exit(int)方法
  1. 该方法会直接停止JVM,不单单停止一个线程,杀伤力太大

线程终止案例

小明是大强的秘书,负责记录会议内容,小明会每1秒记录一次重要讲话内容,当会议结束后,就终止记录工作,但是在终止时会再整理一下会议内容

会议线程:

public class MeetingThread {

// 开会
public void meeting() {
while (true) {
// 判断是否结束会议
if(Thread.currentThread().isInterrupted()) {
System.out.println("会议结束,整理会议记录");
break;
}

// 每1秒记录一次重要内容
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "记录会议内容......");
} catch (InterruptedException e) {
// 调用sleep,join,wait等方法,发生异常,打断标记会被重新设置为false,所以需要再次打断
Thread.currentThread().interrupt();
}
}
}
}

测试类:

public class MeetingThreadMain {
public static void main(String[] args) throws InterruptedException {
MeetingThread meeting = new MeetingThread();

// 小明线程
Thread xiaoming = new Thread(() -> {
// 记录会议内容
meeting.meeting();
},"小明:");

xiaoming.start();

// 5秒后结束会议
Thread.sleep(5000);
// 打断小明线程
xiaoming.interrupt();
System.out.println("会议结束......");
}
}

运行结果:

如果没有在catch中重新打断线程,则会不断记录下去,记住:当线程中调用了sleep,wait,join方法时,线程的打断标记会被重置,需要在catch块中重新打断

isInterrupted 和 interrupted区别

文章开头介绍过,两个方法都是判断线程的打断状态,interrupted 是Thread类的静态方法,获取打断状态之后会重置打断状态为false,而isInterrupted是Thread对象的方法,非静态方法不会重置打断状态

isInterrupted 方法

interrupted方法

发现查看小明线程状态时已经变为 false

注意:Thread.interrupted查看的是正在执行的线程的状态,而isInterrupted是可以指定线程的,根据线程变量名查看状态

LockSupport

LockSupport是JUC中一个工具类,构造方法私有,并且不存在获取实例的静态方法,提供了一堆静态方法帮助完成对线程的操作,主要是为了暂停和恢复线程,主要方法有两个:

  • park():暂停线程
  • unpark(Thread thread):恢复指定线程对象运行

park停止当前线程

import java.util.concurrent.locks.LockSupport;

public class LockSupportMain {
public static void main(String[] args) throws InterruptedException {

Thread t1 = new Thread(() -> {
Thread current = Thread.currentThread();
System.out.println(current.getName() + "启动,被park");
// 停止当前线程
LockSupport.park();
System.out.println(current.getName() + "park之后");
// 输出线程状态
System.out.println(current.getName() + "打断状态" + current.isInterrupted());
}, "t1");
t1.start();
}
}

运行结果:发现线程执行启动后就停止,因为遇到了LockSupport.park();导致t1线程停止运行,但是线程还未结束,所以程序并未停止【左侧红色小框标识】

打断park线程:如下图,在main线程中,等待 1秒 之后,执行t1.interrupt();,打断t1线程,此时t1线程已经被停止运行,发现调用中断方法之后,t1线程又继续执行,并且打断标记为true

park细节

如果park之后,阻塞线程,继续执行,再次调用LockSupport.park();方法阻塞线程时无效,如下进行阻塞二次:

import java.util.concurrent.locks.LockSupport;

public class LockSupportMain {
public static void main(String[] args) throws InterruptedException {

Thread t1 = new Thread(() -> {
Thread current = Thread.currentThread();
System.out.println(current.getName() + "启动,被park");
// 停止当前线程
LockSupport.park();
System.out.println(current.getName() + "park之后");
// 输出线程状态
System.out.println(current.getName() + "打断状态" + current.isInterrupted());
// 再次打断线程
LockSupport.park();
System.out.println(current.getName() + "再次打断无效");
}, "t1");
t1.start();
// 停止1S后,打断t1线程
Thread.sleep(1000);
t1.interrupt();
}
}

运行结果:

如果你还想再次打断,可以调用一次 Thread.interrupted() 获取线程打断状态,并且再设置为false,发现已成功阻塞

unpark:恢复指定线程对象

  • t1线程执行2S后被阻塞
  • 主线程中等待1S后唤醒t1线程

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.locks.LockSupport;

public class LockSupportMain {
public static void main(String[] args) throws InterruptedException {

Thread t1 = new Thread(() -> {
Thread current = Thread.currentThread();
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + "-" + current.getName() + ":启动");
try {
// 睡眠1秒后停止线程
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + "-" + current.getName() + ":park");
// 停止当前线程
LockSupport.park();
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + "-" + current.getName() + ":继续执行");
}, "t1");
// 启动t1线程
t1.start();
// 停止2S
Thread.sleep(2000);
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + "-唤醒t1线程");
// 唤醒t1线程
LockSupport.unpark(t1);
}
}

执行后发现:t1线程被正常唤醒,继续执行

如果我们调换一下阻塞和唤醒的顺序,是否仍然可以正常唤醒呢?也就是:先唤醒t1线程,t1线程再进入阻塞状态,发现仍然可以唤醒成功

为什么可以先调用unpark在调用park呢?

这是一道高频面试题,在下边也有总结,在这里我们不妨先分析一下,这涉及到了 park和unpark原理

当我们调用park方法时其实调用的是 sun.misc.Unsafe UNSAFE 类的 park 方法,这里提到【许可证】这个词,就是park和unpark的关键

每个线程都有一个自己的Parker对象,该对象由三部分组成_counter、_cond、_mutex

可以想一下,unpark其实就相当于一个许可,告诉特定工厂你可以继续生产,特定工厂想要park停止生产的时候一看到有许可,就可以会继续运行。因此其执行顺序可以颠倒。

Parker实例:

park方法:

当调用park()时,先尝试能否直接拿到【许可】,即_counter>0,如果获取成功,则把_counter设置为0,并返回

如果不成功,则构造一个ThreadBlockInVM,然后检查_counter是不是>0,如果是,则把_counter设置为0,并返回

否则,再判断等待的时间,然后再调用pthread_cond_wait函数等待,如果等待返回,则把_counter设置为0并返回:

这就是整个park的过程,总结来说就是消耗【许可】的过程。

unpark方法

JDK源码unpark方法其实也是调用了sun.misc.Unsafe UNSAFE类的unpark方法,注释的意思是给线程【许可证】

当调用unpark()时,直接设置_counter为1,如果_counter之前的值是0,则还要调用pthread_cond_signal唤醒在park中等待的线程:

_counter的值最大为1,即使多次调用unpark()许可证的个数也最多是1

即使我们先调用了unpark,再调用park也是可以正常唤醒线程的,因为unpark获取了一个许可证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

思考:如果唤醒两次后阻塞两次,会是什么结果呢?

许可的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个许可;而调用两次park却需要消费两个许可,证不够,不能放行。线程就会阻塞

总结

  • 调用park()时判断许可是否大于0,如果大于0继续运行,如果不大于0线程就进入阻塞,此时会再创建一个 ThreadBlockInVM 许可是否大于0,如果大于0就将许可设置为0,放行运行
  • 调用unpark()将许可设置为1,无论调用多少次都是1,如果许可为0,则还会调用 pthread_cond_signal唤醒在park中的线程
  • 先调用unpark,再调用park,线程仍然可以被唤醒继续执行

根据合集中《Java线程通信》一文,我们将线程阻塞和唤醒的4种方案介绍完毕,如果在工作或者面试中碰到一定要想起来使用方法和细节

文章出自:石添的编程哲学,如有转载本文请联系【石添的编程哲学】今日头条号。

责任编辑:武晓燕 来源: 今日头条
相关推荐

2023-05-12 14:14:00

Java线程中断

2021-06-04 10:52:51

kubernetes场景容器

2023-12-20 10:04:45

线程池Java

2024-04-23 09:35:27

线程终止C#多线程编程

2023-12-21 10:26:30

​​Prettier

2022-05-13 08:48:50

React组件TypeScrip

2012-02-29 13:39:18

AndroidGoogle

2021-06-25 15:53:25

Kubernetes程序技巧

2022-03-11 12:14:43

CSS代码前端

2021-12-29 17:24:16

Kubernetes集群事件

2024-11-25 13:49:00

2021-02-23 08:02:23

线程volatileinterrupt

2018-07-12 14:20:33

SQLSQL查询编写

2022-06-28 08:01:26

hook状态管理state

2024-05-24 10:51:51

框架Java

2024-02-23 08:57:42

Python设计模式编程语言

2020-04-03 14:55:39

Python 代码编程

2022-03-08 06:41:35

css代码

2018-07-11 20:29:19

数据库MySQLroot密码

2021-01-20 08:26:16

中间件技术spring
点赞
收藏

51CTO技术栈公众号