即使我们先调用了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();
}
}
运行结果:妈妈线程做饭,当小明线程打断之后,妈妈线程就收到打断信息,停止运行
通过代码我们发现,其实线程的终止权在被打断的线程中,通过判断打断标记来控制是否终止,也就是小明不让妈妈做饭,妈妈可以不做也可以继续做。
停止线程其他方法
- 调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如关闭文件数据流,关闭数据库连接等。
- 调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。
- 该方法会直接停止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种方案介绍完毕,如果在工作或者面试中碰到一定要想起来使用方法和细节
文章出自:石添的编程哲学,如有转载本文请联系【石添的编程哲学】今日头条号。