目录
〇、Java线程中断与阻塞的区别
0.1 线程中断
0.2 线程阻塞
一、线程的中断
二、中断方法
2.1 void interrupt()
2.1.1 可中断的阻塞
2.1.2 不可中断的阻塞
2.1.3 实践案例
2.2 boolean isInterrupted()
2.3 boolean interrupted()
2.4 代码案例
三、源码分析
3.1 interrupt()方法源码
3.2 isInterrupted()方法源码
3.2 interrupted()方法源码
四、interrupt()中断行为研究
4.1 原理简单讲解
4.2 调用LockSupport.park()与LockSupport.unpark()
4.2.1 park/unpark实现的伪代码
4.2.2 park/unpark的实验
4.3 interrupt()与LockSupport.park()
4.3.1 interrupt()实现的伪代码
4.3.2 interrupt()实验
4.4 interrupt()与Thread.sleep()
4.4.1 sleep()实现的伪代码
4.4.2 sleep()实验
4.5 wait/join 效果同sleep
4.5.1 wait实验
4.5.2 join实验
4.6 总结
五、Java中实现中断线程的几种方式
5.1 中断
5.2 如何安全的结束一个正在运行的线程
5.2.1 Thread类相关的方法
5.3 使用 interrupt 方法安全中断线程
5.3.1 使用 interrupt() + InterruptedException来中断线程
5.3.2 使用 interrupt() + isInterrupted()来中断线程
5.4 总结
5.5 常见模式——两阶段中止模式
5.5.1 使用场景
5.5.2 代码展示
〇、Java线程中断与阻塞的区别
对于很多刚接触编程的人来说,对于线程中断和线程阻塞两个概念,经常性是混淆起来用,单纯地认为线程中断与线程阻塞的概念是一致的,都是值线程运行状态的停止。其实这个观点是错误的,两者之前有很大的区别,本文就再最开始先着重介绍下两者之间的区别。
0.1 线程中断
在一个线程正常结束之前,如果被强制终止,那么就有可能造成一些比较严重的后果,设想一下如果现在有一个线程持有同步锁,然后在没有释放锁资源的情况下被强制休眠,那么这就造成了其他线程无法访问同步代码块。因此我们可以看到在 Java 中类似 Thread#stop() 方法被标为 @Deprecated。
针对上述情况,我们不能直接将线程给终止掉,但有时又必须将让线程停止运行某些代码,那么此时我们必须有一种机制让线程知道它该停止了。Java 为我们提供了一个比较优雅的做法,即可以通过 Thread#interrupt() 给线程该线程一个标志位,让该线程自己决定该怎么办。
接下来就用代码来延时下 interrupt() 的作用:
public class InterruptDemo {
static class MyThread implements Runnable {
@Override
public void run() {
for (int i= 0; !Thread.currentThread().isInterrupted() && i < 200000; i++) {
System.out.println(Thread.currentThread().getName() + ":i = " + i);
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread myThread = new Thread(new MyThread());
myThread.start();
// 让线程运行一段时间
Thread.sleep(5);
myThread.interrupt();
// 等待 myThread 运行停止
myThread.join();
System.out.println("end");
}
}
以上代码的运行结果如下:
可以看到,当前线程并没有按 for 循环中的结束量 20000 去跑,而是在被中断后,停止了当前了 for 循环。所以我们可以利用 interrupt 配置线程使用,使得线程在一定的位置停止下来。
不过到这里可能会让人产生一些疑惑,因为在这里看起来,当前线程像是被阻塞掉了,其实并不是的,我们可以利用下面这段代码来演示下:
public class InterruptDemo {
static class MyThread implements Runnable {
@Override
public void run() {
for (int i= 0; i < 200000; i++) {
System.out.println(Thread.currentThread().getName() + ":i = " + i);
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread myThread = new Thread(new MyThread());
myThread.start();
// 让线程运行一段时间
Thread.sleep(5);
myThread.interrupt();
// 等待 myThread 运行停止
myThread.join();
System.out.println("end");
}
}
上面这段代码的运行结果如下:
可见,线程一直打印到 20000,执行完毕后推出线程,并没有像我们预料中在某处中断。所以我们可以得出结论:单纯用 interrupt() 中断线程方法并不能停止当前正在运行的线程,需要配合其他方法才能正确停止线程。
了解完中断的基本概念后,线程的中断还有需要其他需要注意的点:
- 设置线程中断后,线程内调用 wait()、join()、slepp() 方法中的一种,都会抛出 InterruptedException 异常,且中断标志位被清除,重新设置为 false;
- 当线程被阻塞,比如调用了上述三个方法之一,那么此时调用它的 interrupt() 方法,也会产生一个 InterruptedException 异常。因为没有占有 CPU 的线程是无法给自己设置中断状态位置的;
- 尝试获取一个内部锁的操作(进入一个 synchronized 块)是不能被中断的,但是 ReentrantLock 支持可中断的获取模式:tryLock(long time, TimeUnit unit);
- 当代码调用中需要抛出一个 InterruptedException,捕获之后,要么继续往上抛,要么重置中断状态,这是最安全的做法。
0.2 线程阻塞
上面讲完了线程中断,它其实只是一个标志位,并不能让线程真正的停止下来,那么接下来就来介绍如何真正让线程停止下来。
对于这个问题,Java 中提供了一个较为底层的并发工具类:LockSupport,该类中的核心方法有两个:park(Object blocker) 以及 unpark(Thread thred),前者表示阻塞指定线程,后者表示唤醒指定的线程。
// java.util.concurrent.locks.LockSupport
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
该方法在 Java 的语言层面上比较简单,最终也是去调用 UNSAFE 中的 native 方法。真正涉及到底层的东西需要去理解 JVM 的源码,这里就不做太多的介绍。不过我们可以用一个简单的例子来演示下这两个方法:
public class LockSupportDemo {
static class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始执行");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "执行结束");
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyThread(), "线程:MyThread");
thread.start();
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "主线程执行中");
LockSupport.unpark(thread);
System.out.println(Thread.currentThread().getName() + "主线程执行结束");
}
}
上述代码的执行结果为:
线程:MyThread开始执行
main主线程执行中
线程:MyThread执行结束
main主线程执行结束
可以看到,myThread 线程在开始执行后停止了下来,等到主线程重新调用 LockSupport.unpark(thread) 后才重新开始执行。
一、线程的中断
下面我们进入本文的正题,开始向西讲解Java中线程的中断及其原理。
Java的中断是一种协作机制,也就是说通过中断并不能直接中断另外一个线程,而需要被中断的线程自己处理中断。
在Java的中断模型中,每个线程都有一个boolean类型的标识,代表着是否有中断请求(该请求可以来自所有线程,包括被中断的线程本身)。例如,当线程t1想中断线程t2,只需要在线程t1中将线程t2对象的中断标识置为true,然后线程2可以选择在合适的时候处理该中断请求,甚至可以不理会该请求,就像这个线程没有被中断一样。
官方一点的表述:
Java中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
二、中断方法
Thread类中提供了线程中断相关的方法,主要是下面的3方法,他们具体的作用见下面的表格:
方法名 | 介绍 |
void interrupt() | 中断线程,设置线程的中断位为true |
boolean isInterrupted() | 检查线程的中断标记位,true-中断状态, false-非中断状态 |
static boolean interrupted() | 静态方法,返回当前线程的中断标记位,同时清除中断标记,改为false。比如当前线程已中断,调用interrupted(),返回true, 同时将当前线程的中断标记位改为false, 再次调用interrupted(),会发现返回false |
记忆方法推荐:
- interrupt()是一个动词,表示中断线程。
- Interrupted()是一个形容词,用于检查线程的中断位并修改中断位,而isInterrupted()方法只是简单的检查,interrupted()在检查的同时还会对中断位进行操作。
2.1 void interrupt()
方法原型:
/**
* 中断此线程。
* <p>线程可以中断自身,这是允许的。在这种情况下,不用进行安全性验证({@link #checkAccess() checkAccess} 方法检测)
* <p>若当前线程由于 wait() 方法阻塞,或者由于join()、sleep()方法,然后线程的中断状态将被清除,并且将收到 {@link InterruptedException}。
* <p>如果线程由于 IO操作({@link java.nio.channels.InterruptibleChannel InterruptibleChannel})阻塞,那么通道 channel 将会关闭,
* 并且线程的中断状态将被设置,线程将收到一个 {@link java.nio.channels.ClosedByInterruptException} 异常。
* <p>如果线程由于在 {@link java.nio.channels.Selector} 中而阻塞,那么线程的中断状态将会被设置,它将立即从选择操作中返回。
*该值可能是一个非零值,就像调用选择器的{@link java.nio.channels.Selector#wakeupakeup}方法一样。
*
* <p>如果上述条件均不成立,则将设置该线程的中断状态。</p>
* <p>中断未运行的线程不必产生任何作用。
* @throws SecurityException 如果当前线程无法修改此线程
*/
public void interrupt()
很多人看到 interrupt() 方法,认为“中断”线程不就是让线程停止嘛。实际上, interrupt() 方法实现的根本就不是这个效果, interrupt()方法更像是发出一个信号,这个信号会改变线程的一个标识位属性(中断标识),对于这个信号如何进行响应则是无法确定的(可以有不同的处理逻辑)。很多时候调用 interrupt() 方法非但不是为了停止线程,反而是为了让线程继续运行下去。设置线程中断不影响线程的继续执行
interrupt() 方法的作用:设置该线程的中断标志为true并立即返回(该线程并不一定是当前线程,而是指调用该方法的Thread实例所代表的线程),但线程实际上并没有被中断而会继续向下执行,会由用户自己决定要不要终止线程以及什么时候终止线程;如果线程因为调用了wait系列函数、join方法或者sleep方法而被阻塞挂起,其他线程调用该线程的interrupt()方法会使该线程抛出InterruptedException异常而返回。
interrupt()是实例方法,是调用该方法的对象所表示的那个线程的interrupt()。
2.1.1 可中断的阻塞
针对线程处于由sleep, wait, join,方法调用产生的阻塞状态时,调用interrupt方法,会抛出异常InterruptedException,同时会清除中断标记位,自动改为false。
LockSupport.park也会相应中断,但是不会抛出异常,也不会清空中断标记。
一般情况下,抛出异常时,会清空Thread的interrupt状态,在编程时需要注意;
2.1.2 不可中断的阻塞
- java.io包中的同步Socket I/O
- java.io包中的同步I/O
- Selector的异步I/O
- Lock.lock()方法不会响应中断;Lock.lockInterruptibly()方法则会响应中断并抛出异常,区别在于park()等待被唤醒时lock会继续执行park()来等待锁,而 lockInterruptibly会抛出异常;
- sychronized加的锁。synchronized被唤醒后会尝试获取锁,失败则会通过循环继续park()等待,因此实际上是不会被interrupt()中断的;
2.1.3 实践案例
2.1.3.1 中断sleep、wait、join等方法
private static void test1() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("error", e);
}
}, "t1");
t1.start();
Thread.sleep(500);
t1.interrupt();
Thread.sleep(100);
log.info(" interrupt status : {}", t1.isInterrupted());
}
结论: 阻塞方法sleep响应中断,抛出InterruptedException,t1线程也就不会再继续向下执行了,同时清除中断标记位为false。
2.1.3.2 中断LockSupport.park方法
public static void test3() throws InterruptedException {
Thread t3 = new Thread(() -> {
log.debug("t3 park.....");
LockSupport.park();
log.debug("t3 unpark.....");
log.debug("interrupt status: [{}]", Thread.currentThread().isInterrupted());
log.debug("t3 第二次 park.....");
LockSupport.park();
log.debug("t3 中断位为true, park失效.....");
}, "t3");
t3.start();
Thread.sleep(1000);
t3.interrupt();
}
结论: 阻塞方法park响应中断,即t3被LockSupport.park()阻塞,然后主线程调用t3.interrupt(),park()方法就响应中断,结束阻塞,并且不会抛出异常,t3线程继续向下执行,同时不会清除中断标记位,仍为true。
2.1.3.3 中断普通方法
private static void test2() throws InterruptedException {
Thread t2 = new Thread(() -> {
while (true) {
boolean isInterrupted = Thread.currentThread().isInterrupted();
if (isInterrupted) {
log.info("interrupt status: {}", isInterrupted);
break;
}
}
}, "t2");
t2.start();
Thread.sleep(500);
t2.interrupt();
Thread.sleep(100);
log.info(" thread status, {}, interrupt status : {}", t2.getState(), t2.isInterrupted());
}
结论: 打断正常运行的线程,线程要自己决定是否响应中断,在线程执行过程中不会清空中断状态,但是线程结束后,会重置线程的中断状态位。
2.1.3.4 中断IO相关方法
interrupt方法源码中有一段代码如下:
private volatile Interruptible blocker;
private final Object blockerLock = new Object();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
其中blocker是Thread的成员变量,Thread提供了blockedOn方法可以设置blocker:
void blockedOn(Interruptible b) {
synchronized (blockerLock) {
blocker = b;
}
}
如果一个nio通道实现了InterruptibleChannel接口,就可以响应interrupt()中断,其原理就在InterruptibleChannel接口的抽象实现类AbstractInterruptibleChannel的方法begin()中:
protected final void begin() {
if (interruptor == null) {
interruptor = new Interruptible() {
public void interrupt(Thread target) {
synchronized (closeLock) {
if (!open)
return;
open = false;
interrupted = target;
try {
AbstractInterruptibleChannel.this.implCloseChannel();
} catch (IOException x) { }
}
}};
}
blockedOn(interruptor);//设置当前线程的blocker为interruptor
Thread me = Thread.currentThread();
if (me.isInterrupted())
interruptor.interrupt(me);
}
protected final void end(boolean completed)
throws AsynchronousCloseException
{
blockedOn(null);//设置当前线程的blocker为null
Thread interrupted = this.interrupted;
//如果发生中断,Thread.interrupt方法会调用Interruptible的interrupt方法,
//设置this.interrupted为当前线程
if (interrupted != null && interrupted == Thread.currentThread()) {
interrupted = null;
throw new ClosedByInterruptException();
}
if (!completed && !open)
throw new AsynchronousCloseException();
}
//Class java.nio.channels.Channels.WritableByteChannelImpl
public int write(ByteBuffer src) throws IOException {
......
try {
begin();
out.write(buf, 0, bytesToWrite);
finally {
end(bytesToWrite > 0);
}
......
}
//Class java.nio.channels.Channels.ReadableByteChannelImpl
public int read(ByteBuffer dst) throws IOException {
......
try {
begin();
bytesRead = in.read(buf, 0, bytesToRead);
finally {
end(bytesRead > 0);
}
......
}
以上述代码为例,nio通道的ReadableByteChannel每次执行阻塞方法read()前,都会执行begin(),把Interruptible回调接口注册到当前线程上。当线程中断时,Thread.interrupt()触发回调接口Interruptible关闭io通道,导致read方法返回,最后在finally块中执行end()方法检查中断标记,抛出ClosedByInterruptException;
Selector的实现类似,所以它也可以响应中断:
//java.nio.channels.spi.AbstractSelector
protected final void begin() {
if (interruptor == null) {
interruptor = new Interruptible() {
public void interrupt(Thread ignore) {
AbstractSelector.this.wakeup();
}};
}
AbstractInterruptibleChannel.blockedOn(interruptor);
Thread me = Thread.currentThread();
if (me.isInterrupted())
interruptor.interrupt(me);
}
protected final void end() {
AbstractInterruptibleChannel.blockedOn(null);
}
//sun.nio.ch.class EPollSelectorImpl
protected int doSelect(long timeout) throws IOException {
......
try {
begin();
pollWrapper.poll(timeout);
} finally {
end();
}
......
}
可以看到当发生中断时会调用wakeup方法唤醒poll方法,但并不会抛出中断异常;
2.2 boolean isInterrupted()
方法原型:
/**
* 测试此线程是否已被中断。线程的中断状态不受此方法的影响。
* 如果中断时,线程并没有存活,那么该方法返回 false。意思就是,如果线程还没有 start 启动,或者已经消亡,那么返回依然是 false.
* @return 如果该线程已被中断,返回true;否则返回 false
*/
public boolean isInterrupted()
isInterrupted()方法的作用:只判断此线程(此线程指的是调用isInterrupted()方法的Thread实例所代表的线程)是否被中断 ,是则返回true,否则返回false,不清除中断状态。
如果线程还没有 start 启动,或者已经消亡,那么返回依然是 false。中断状态只代表是否有线程调用中断方法,并不代表这个线程是否在运行。
isInterrupted()是实例方法,是调用该方法的对象所表示的那个线程的isInterrupted()。
2.3 boolean interrupted()
方法原型:
/**
* 测试当前线程是否已被中断。
* 通过此方法可以清除线程的中断状态.
* 换句话说,如果此方法要连续调用两次,则第二个调用将返回false(除非当前线程在第一个调用清除了它的中断状态之后,且在第二个调用对其进行检查之前再次中断)
* 如果中断时,线程并没有存活(还未启动),那么该方法返回 false
* @return 如果该线程已被中断,返回true;否则返回 false
*/
public static boolean interrupted()
interrupted()方法的作用:判断当前线程(注意,这里指的是当前线程,不是调用该方法的Thread实例所代表的线程)是否被中断(检查中断标志),返回一个boolean(当前的中断标志),被中断则返回true,否则返回false。并清除中断状态(将中断标志设置为false)。因为该方法是static方法,内部是获取当前调用线程的中断标志而不是调用interrupted()方法的实例对象的中断标志。
这里有一点需要注意,调用interrupted()方法时,它会先记录下当前的中断标志位,然后才会去清除中断状态,也就是将中断标志位设置为false,最后返回之前记录的中断标志位。也就是说如果在调用interrupted()方法前当前线程已经中断了(中断标志为true),那么第一次调用interrupted()方法返回的就是true,但是第一次调用之后当前线程的中断标志就被interrupted()方法设置为false了,所以第二次再调用interrupted()方法时中断状态已经被清除,将返回一个false。
2.4 代码案例
定义一个MyThread类,继承Thread,如下:
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("i =" + (i + 1));
}
}
}
在main方法中测试:
public class Do {
public static void main(String[] args ) {
MyThread thread = new MyThread();
thread.start();
thread.interrupt();
System.out.println("第一次调用thread.isInterrupted():" + thread.isInterrupted());
System.out.println("第二次调用thread.isInterrupted():" + thread.isInterrupted());
System.out.println("thread是否存活:" + thread.isAlive());
}
}
输出如下:
从结果可以看出调用interrupt()方法后,线程仍在继续运行,并未停止,但已经给线程设置了中断标志,两个isInterrupted()方法都会输出true,也说明isInterrupted()方法并不会清除中断状态。
下面我们把代码修改一下,多加两行调用interrupted()方法:
public class Do {
public static void main(String[] args ) {
MyThread thread=new MyThread();
thread.start();
thread.interrupt();
System.out.println("第一次调用thread.isInterrupted():"+thread.isInterrupted());
System.out.println("第二次调用thread.isInterrupted():"+thread.isInterrupted());
//测试interrupted()函数
System.out.println("第一次调用thread.interrupted():"+thread.interrupted());
System.out.println("第二次调用thread.interrupted():"+thread.interrupted());
System.out.println("thread是否存活:"+thread.isAlive());
}
}
输出如下:
从输出结果看,可能会有疑惑,为什么后面两个interrupted方法输出的都是false,而不是预料中的一个true、一个false?
注意!这是一个坑!上面说到,interrupted()方法测试的是当前线程是否被中断,是当前线程!这里当前线程是main线程,而thread.interrupt()中断的是thread对象代表的线程,这里的此线程就是thread对象代表的线程。所以当前线程main从未被中断过,尽管interrupted()方法是以thread.interrupted()的形式被调用,但它检测的仍然是main线程而不是检测thread对象代表的线程,所以thread.interrupted()在这里相当于main.interrupted()。对于这点,下面我们再修改进行测试。
Thread.currentThread()函数可以获取当前线程的线程对象,下面代码中获取的是main线程
public class Do {
public static void main(String[] args ) throws InterruptedException {
Thread.currentThread().interrupt();
System.out.println("第一次调用Thread.currentThread().isInterrupt():" + Thread.currentThread().isInterrupted());
System.out.println("第一次调用thread.interrupted():" + Thread.currentThread().interrupted());
System.out.println("第二次调用thread.interrupted():" + Thread.currentThread().interrupted());
}
}
这里都是针对当前线程在操作,如果interrupted()方法有检测中断并清除中断状态的作用,预料中的输出应该是true-true-false,实际输出如下:
结果证明猜想是正确的。
如果想要实现调用interrupt()方法真正地终止线程,则可以在线程的run方法中做处理即可,比如检测到中断信号后直接跳出run()方法使线程结束,视具体情况而定,下面是一个例子。
修改MyThread类:
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("i =" + (i + 1));
if (this.isInterrupted()){
System.out.println("通过this.isInterrupted()检测到中断");
System.out.println("第一个interrupted()" + this.interrupted());
System.out.println("第二个interrupted()" + this.interrupted());
break;
}
}
System.out.println("因为检测到中断,所以跳出循环,线程到这里结束,因为后面没有内容了");
}
}
测试MyThread:
public class Do {
public static void main(String[] args ) throws InterruptedException {
MyThread myThread=new MyThread();
myThread.start();
myThread.interrupt();
//sleep等待一秒,等myThread运行完
Thread.currentThread().sleep(1000);
System.out.println("myThread线程是否存活:"+myThread.isAlive());
}
}
结果:
最后总结,关于这三个方法,interrupt()是给线程设置中断标志;interrupted()是检测中断并清除中断状态;isInterrupted()只检测中断。还有重要的一点就是interrupted()作用于当前线程,interrupt()和isInterrupted()作用于此线程,即代码中调用此方法的实例所代表的线程。
三、源码分析
我们会发现这几个Java方法,底层其实是调用native方法实现的,所以线程的中断状态,并不是由 Java 来决定。实际上,Thread 类中并没有维护线程的中断状态。线程中断状态不是 Thread 类的标志位,而是操作系统中对线程的中断标志。
3.1 interrupt()方法源码
源码:
public void interrupt() {
// 除非当前线程是自己中断自己,否则将调用此线程的 checkAccess 方法,这可能导致抛 SecurityException。
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
public final void checkAccess() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkAccess(this);
}
}
本质调用的就是这个native方法,interrupt的功能都是在这个native方法中实现的
// 中断线程的底层方法
private native void interrupt0();
interrupt中文是打断的意思,意思是可以打断中止正在运行的线程,比如:
- Object#wait()、Thread#join()、Thread#sleep()这些方法运行后,线程的状态是WAITING或TIMED_WAITING,这时候打断这些线程,就会抛出InterruptedException异常,使线程的状态直接到TERMINATED。且中断标志被清除,重新设置为false
- 如果线程堵塞在java.nio.channels.InterruptibleChannel的IO上,我们中断当前线程,连接(Channel)会被关闭,线程会被设置为中断状态,并抛出ClosedByInterruptException异常;
- 如果线程堵塞在java.nio.channels.Selector上,线程被置为中断状态,select方法会立即返回,就像调用了选择器的 wakeup 方法一样。
如果上面三种情况都没有发生,就会去调用native方法interrupt0(),当前线程的中断标志就会被设置为true。
中断非活动的线程不会有任何的反应。
我们举一个例子来说明如何打断WAITING的线程,代码如下:
public class ThreadInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " begin run");
try {
System.out.println("子线程开始沉睡30s");
Thread.sleep(30000L);
} catch (InterruptedException e) {
System.out.println("子线程被打断");
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end run");
}
});
// 开一个子线程去执行
thread.start();
Thread.sleep(1000L);
System.out.println("主线程等待1s后,发现子线程还没有运行成功,打断子线程");
thread.interrupt();
}
}
例子主要说的是,主线程会等待子线程执行1s,如果1s内线程还没有执行完,就会打断子线程,子线程被打断后,会抛出InterruptedException异常,执行结束。
3.2 isInterrupted()方法源码
源码:
public boolean isInterrupted() {
// 传递 false 说明不清除中断标志
return isInterrupted(false);
}
本质调用的是native方法isInterrupted,是通过这个native方法实现的功能
private native boolean isInterrupted(boolean ClearInterrupted);
检测线程是否被中断,是则返回true,否则返回false。
3.2 interrupted()方法源码
源码:
public static boolean interrupted() {
// 传递 true 说明清除中断标志
return currentThread().isInterrupted(true);
}
本质调用的是native方法isInterrupted
/**
* 测试某些线程是否已被中断。线程的中断状态不受此方法的影响。
* ClearInterrupted参数决定线程中断状态是否被重置,若为true则重置。
*/
private native boolean isInterrupted(boolean ClearInterrupted);
检测当前线程是否被中断,返回值同上 isInterrupted() ,不同的是,如果发现当前线程被中断,会清除中断标志;换句话说,如果要连续两次调用此方法,则第二个调用将返回false(除非在第一次调用清除了其中断状态之后,在第二次调用对其进行检查之前,当前线程再次被中断)。
该方法是static方法,内部是获取当前调用线程的中断标志而不是调用interrupted()方法的实例对象的中断标志。
四、interrupt()中断行为研究
4.1 原理简单讲解
首先声明,本文不会去贴native方法的cpp实现,而是以伪代码的形式来理解这些native方法。
- Thread对象的native实现里有一个成员代表线程的中断状态,我们可以认为它是一个bool型的变量。初始为false。
- Thread对象的native实现里有一个成员代表线程是否可以阻塞的许可permit,我们可以认为它是一个int型的变量,但它的值只能为0或1。当为1时,再累加也会维持1。初始为0。
4.2 调用LockSupport.park()与LockSupport.unpark()
4.2.1 park/unpark实现的伪代码
下面将以伪代码的实现来说明park/unpark的底层实现。
1、park()
park() {
if(permit > 0) {
permit = 0;
return;
}
if(中断状态 == true) {
return;
}
// 阻塞过程中还会持续地监控中断状态,如果中断状态为true,则结束阻塞
阻塞当前线程; // 将来会从这里被唤醒
// 在最后都会消费掉permit
if(permit > 0) {
permit = 0;
}
}
可见,只要permit为1或者中断状态为true,那么执行park就不能够阻塞线程。park只可能消耗掉permit,但不会去消耗掉中断状态。所以能够使用park()阻塞的线程,它的premit一定为0,并且中断状态一定是false。调用过park()的线程,不管是有没有进行阻塞,不管是因为中断而结束的阻塞,还是因为unpark而结束的阻塞,只要调用了park()方法,到最后permit一定为0(因为在开始和结束位置都去消费了permit)。
2、unpark()
unpark(Thread thread) {
if(permit < 1) {
permit = 1;
if(thread处于阻塞状态)
唤醒线程thread;
}
}
unpark一定会将permit置为1,如果此时线程处于阻塞状态,再将其唤醒。从实现可见,无论调用几次unpark,permit只能为1。调用过unpark()的线程,最后permit一定为1。
4.2.2 park/unpark的实验
实现1:
public class test3 {
public static void main(String[] args) throws InterruptedException {
LockSupport.park(); //因为此时permit为0且中断状态为false,所以阻塞
}
}
上面程序执行后,程序不会运行结束,main线程阻塞。
原因是,线程默认的permit是0,中断状态为false,所以会阻塞当前线程;。
实验2:
public class test3 {
public static void main(String[] args) throws InterruptedException {
LockSupport.unpark(Thread.currentThread()); //置permit为1
LockSupport.park(); //消耗掉permit后,直接返回了
}
}
上面程序执行后,程序运行结束。
原因是LockSupport.unpark(Thread.currentThread())执行后,会使得main线程的permit为1。而park时发现这个permit为1时,就会消耗掉这个permit,然后直接返回,所以main线程没有阻塞。
实验3:
public class test3 {
public static void main(String[] args) throws InterruptedException {
LockSupport.unpark(Thread.currentThread());
LockSupport.park(); //消耗掉permit后,直接返回了
LockSupport.park(); //此时permit为0,中断状态为false,必然会阻塞
}
}
上面程序执行后,程序不会运行结束,main线程阻塞。
原因是第二次park时,permit为0了,中断状态为false,所以会阻塞当前线程;。
实验4:
public class test3 {
public static void main(String[] args){
Thread main = Thread.currentThread();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程开始睡觉");
try {
Thread.sleep(1000);//睡一下保证是在main线程park后,才去unpark main线程
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"抛出了中断异常");
}
System.out.println("子线程睡醒了,开始unpark main线程");
LockSupport.unpark(main);
}
}).start();
LockSupport.park(); //此时permit为0,中断状态为false,必然会阻塞
//被子线程unpark后,从上一句被唤醒,继续执行。此时permit还是为0,中断状态为false。
LockSupport.park(); //此时permit为0,中断状态为false,必然会阻塞
}
}
上面程序执行后,程序不会运行结束,main线程阻塞。
这个程序和之前的代码基本意思是一样的,只是之前的版本都是先unpark,再park。现在保证是,main线程先park后,再去unpark main线程。
4.3 interrupt()与LockSupport.park()
4.3.1 interrupt()实现的伪代码
interrupt(){
if(中断状态 == false) {
中断状态 = true;
}
unpark(this); //注意这是Thread的成员方法,所以我们可以通过this获得Thread对象
}
interrupt()会设置中断状态为true。注意,interrupt()还会去调用unpark的,unpark()会将permit不为1的设置为1,所以只要是调用过interrupt()的线程,它的permit一定为1。
4.3.2 interrupt()实验
实验1:
public class test3 {
public static void main(String[] args) throws InterruptedException {
Thread.currentThread().interrupt();
LockSupport.park(); //消耗掉permit后,直接返回了
}
}
上面程序执行后,程序运行结束。因为先执行的interrupt,会将permit设置为1,park执行时permit为1,所以将permit置为0后就直接返回了。
实验2:
public class test3 {
public static void main(String[] args) throws InterruptedException {
Thread.currentThread().interrupt();
LockSupport.park(); //消耗掉permit后,直接返回了
LockSupport.park(); //因为中断状态 == true,直接返回了
LockSupport.park(); //同上
}
}
上面程序执行后,程序运行结束。无论调用多少次park都无法阻塞线程,因为此时线程的中断状态为true,函数直接返回了。
实验3:
public class test3 {
public static void main(String[] args) throws InterruptedException {
Thread main = Thread.currentThread();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("马上开始睡觉");
try {
Thread.sleep(1000);//睡一下保证是在main线程阻塞后,才去中断main线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("睡醒了,开始中断main线程");
main.interrupt();
}
}).start();
LockSupport.park(); //此时permit为0,中断状态为false,必然会阻塞
// 被子线程中断后,从上一句被唤醒,继续执行。此时permit为0,中断状态为true。
LockSupport.park(); //因为中断状态 == true,直接返回了
LockSupport.park(); //同上
}
}
上面程序执行后,程序运行结束。
这个程序和之前的代码基本意思是一样的,只是之前的版本都是先中断,再park。现在保证是,main线程先阻塞后,再去中断main线程。
4.4 interrupt()与Thread.sleep()
4.4.1 sleep()实现的伪代码
sleep() {//这里我忽略了参数,假设参数是大于0的即可
// 如果中断状态为true,就直接清空中断状态并抛出异常,不去做休眠操作
if(中断状态 == true) {
中断状态 = false;
throw new InterruptedException();
}
// 在休眠过程中还是会持续监控中断状态,如果中断状态为true,就会直接结束阻塞
线程开始休眠;
// 并且在最后还会清空中断状态并抛出异常
if(中断状态 == true) {
中断状态 = false;
throw new InterruptedException();
}
}
sleep()会去检测中断状态,如果检测到了,那就消耗掉中断状态后,抛出中断异常。但sleep()不会去修改permit。调用过sleep()的线程,不管是有没有真的休眠了,还是说因为休眠时间到了结束了休眠状态,或者是因为中断而结束的休眠状态,只要调用过sleep()方法,那么它的中断状态最后一定为false。因为sleep方法在开始和结尾都去清空了中断状态并抛出异常。
4.4.2 sleep()实验
实验1:
public class test3 {
public static void main(String[] args){
Thread.currentThread().interrupt();
try {
Thread.sleep(1000); // 消耗掉中断状态后,抛出异常
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上面程序执行后,抛出异常,程序运行结束。
实验2:
public class test3 {
public static void main(String[] args){
Thread.currentThread().interrupt();
try {
Thread.sleep(1000); // 消耗掉中断状态后,抛出异常
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.park(); //消耗掉permit
}
}
上面程序执行后,抛出异常,异常被catch后主线程继续向下执行到patk(),此时主线程的permit为1,park消耗掉permit就会直接返回,主程序运行结束。
实验3:
public class test3 {
public static void main(String[] args){
Thread.currentThread().interrupt();
try {
Thread.sleep(1000);//消耗掉中断状态
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.park(); //消耗掉permit
LockSupport.park(); //因为此时permit为0且中断状态为false,所以阻塞
}
}
上面程序执行后,抛出异常,异常被catch后主线程继续向下执行,执行第一个park()会将permit消耗,然后继续向下执行到第二个park,因为此时permit为0,并且中断状态也被sleep消耗掉了,所以线程被阻塞,程序不会运行结束。
实验4:
public class test3 {
public static void main(String[] args){
Thread main = Thread.currentThread();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程开始睡觉");
try {
Thread.sleep(3000);//睡一下保证是在main线程sleep后,才去中断main线程
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"抛出了中断异常");
}
System.out.println("子线程睡醒了,开始中断main线程");
main.interrupt();
}
}).start();
try {
System.out.println("主线程开始睡觉");
Thread.sleep(5000); //main线程开始睡觉
// 当被中断唤醒后,会消耗掉中断状态。唤醒后继续执行
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"抛出了中断异常");
}
LockSupport.park(); //消耗掉permit后,直接返回了
LockSupport.park(); //此时permit为0,中断状态为false,必然会阻塞
}
}
上面程序执行后,抛出异常,程序不会运行结束。
这个程序和之前的代码基本意思是一样的,只是之前的版本都是先中断,再sleep。现在保证是,main线程先sleep后,再去中断main线程。
4.5 wait/join 效果同sleep
这两个方法的源码层面的实现也适合sleep基本一致的。
4.5.1 wait实验
public class test3 {
public static void main(String[] args){
Thread.currentThread().interrupt();
Object lock = new Object();
synchronized (lock) {
try {
lock.wait(); //消耗掉中断状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
LockSupport.park(); //消耗掉permit
LockSupport.park(); //此时permit为0,中断状态为false,必然会阻塞
}
}
上面程序执行后,抛出异常,程序不会运行结束。
4.5.2 join实验
public class test3 {
public static void main(String[] args){
Thread.currentThread().interrupt();
Thread thread = new Thread(()->{
while (true) {}
});
thread.start();
try {
thread.join(); //消耗掉中断状态
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.park(); //消耗掉permit
LockSupport.park(); //此时permit为0,中断状态为false,必然会阻塞
}
}
上面程序执行后,抛出异常,程序不会运行结束。通过Dump Threads后,可以发现main处于WAITING (parking)状态,即阻塞状态。
4.6 总结
- park调用后一定会消耗掉permit,无论unpark操作先做还是后做(看上面对park讲解的源码中,在park开始和结尾都执行了一遍消耗permit的操作,所以无论unpark操作在park操作之前还是之后,只要是调用了park,那么现成的permit最后一定是0)。
- 如果中断状态为true,那么park无法阻塞。
- unpark会使得permit为1,并唤醒处于阻塞的线程。
- interrupt()会使得中断状态为true,并调用unpark。
- sleep() / wait() / join()调用后一定会消耗掉中断状态,无论interrupt()操作先做还是后做(看上面对sleep讲解的源码中,在sleep开头和结尾都执行了一遍消耗中断状态的代码,所以无论interrupt操作在sleep之前还是之后,只要是调用了sleep,那么线程的中断状态最后一定是false)。
关于这一点,“如果中断状态为true,那么park无法阻塞”。在AQS源码里的acquireQueued里,由于acquireQueued是阻塞式的抢锁,线程可能重复着 阻塞->被唤醒 的过程,所以在这个过程中,如果遇到了中断,一定要用Thread.interrupted()将中断状态消耗掉,并将这个中断状态暂时保存到一个局部变量中去。不然只要遇到中断一次后,线程在抢锁失败后就无法阻塞了。
五、Java中实现中断线程的几种方式
中断(Interrupt)一个线程意味着在该线程完成任务之前停止其正在进行的一切,有效地中止其当前的操作。本节主要介绍Java中断线程的几种方式,需要的朋友可以参考下。
5.1 中断
中断(Interrupt)一个线程意味着在该线程完成任务之前停止其正在进行的一切,有效地中止其当前的操作。线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序。虽然初次看来它可能显得简单,但是,你必须进行一些预警以实现期望的结果。你最好还是牢记以下的几点告诫。
首先,忘掉Thread.stop方法。虽然它确实停止了一个正在运行的线程,然而,这种方法是不安全也是不受提倡的,这意味着,在未来的JAVA版本中,它将不复存在。
5.2 如何安全的结束一个正在运行的线程
5.2.1 Thread类相关的方法
java.lang.Thread类包含了一些常用的方法,如:start(), stop(), stop(Throwable) ,suspend(), destroy() ,resume()。通过这些方法,我们可以对线程进行方便的操作,但是这些方法中,只有start()方法得到了保留。
在JDK帮助文档以及Sun公司的一篇文章《Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?》中都讲解了舍弃这些方法的原因。
简单来说是因为:使用stop方法虽然可以强行终止正在运行或挂起的线程,但使用stop方法是很危险的,就像突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,因此,并不推荐使用stop方法来终止线程。
那么,我们究竟应该如何停止线程呢?
1、任务中一般都会有循环结构,只要用一个标记控制住循环,就可以结束任务。
2、如果线程处于阻塞状态,无法读取标记,此时可以使用interrupt()方法将线程从阻塞状态强制恢复到运行状态中来(在前面的小节讲过,sleep、wait、join、park等方法都会直接响应中断,也就是执行中断方法后这些方法就会结束掉阻塞状态),让线程具备CPU的执行资格。
下面详细讲解一下这两种方法。
(一):使用退出标志
当run方法执行完后,线程就会退出。但有时run方法是永远不会结束的,如在服务端程序中使用线程进行监听客户端请求,或是其他的需要循环处理的任务。
在这种情况下,一般是将这些任务放在一个循环中,如while循环。如果想使while循环在某一特定条件下退出,最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。
public class test1 {
public static volatile boolean exit =false; //退出标志
public static void main(String[] args) {
new Thread() {
public void run() {
System.out.println("线程启动了");
while (!exit) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程结束了");
}
}.start();
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
exit = true;// 5秒后更改退出标志的值,没有这段代码,线程就一直不能停止
}
}
(二):使用 interrupt 方法
Thread.interrupt()方法:作用是中断线程。将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。它并不像stop方法那样会直接中断一个正在运行的线程。
interrupt()方法只是改变中断状态,不会直接中断一个正在运行的线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法以及LockSupport.park方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常或结束阻塞。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程检查到中断标识,就得以退出阻塞的状态。
更具体的说:
- 如果线程被Object.wait、Thread.join和Thread.sleep三种方法之一阻塞,这三种方法在阻塞时还是会持续监控中断状态,此时调用该线程的interrupt()方法,就会监控到中断状态为true了,这三个方法就会抛出一个 InterruptedException中断异常(该线程必须事先预备好catch处理此异常,否则主线程就会直接异常终止了),从而提早地终结被阻塞状态。
- 如果线程没有被阻塞,这时调用 interrupt()将不起作用,只会将线程的中断标志设置为true,直到线程执行到wait()、sleep()、join()时,就会直接抛出InterruptedException异常而不会去进行阻塞。因为在上面我们也讲过这些阻塞方法会在开始位置查询中断标志,如果中断标志为true,就会直接抛出 InterruptedException异常,而不会去执行阻塞命令。
5.3 使用 interrupt 方法安全中断线程
上一章节我们说过可以用interrupt()来安全的结束正在运行的线程,下面我们就更加详细的讲一下究竟如何用interrupt()来实现这个功能。
5.3.1 使用 interrupt() + InterruptedException来中断线程
首先我们知道当线程处于阻塞状态,如Thread.sleep、wait、IO阻塞等情况时,调用interrupt方法后,sleep等方法将会抛出一个InterruptedException:
public static void main(String[] args) {
Thread thread = new Thread() {
public void run() {
System.out.println("线程启动了");
try {
Thread.sleep(1000 * 100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程结束了");
}
};
thread.start();
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();// 作用是:在线程阻塞时抛出一个中断信号,这样线程就会抛出异常得以退出阻塞的状态
}
5.3.2 使用 interrupt() + isInterrupted()来中断线程
使用 interrupt() + isInterrupted()来安全地中断线程。
方法作用:
- this.interrupted():检查当前线程是否已经中断(静态方法)。如果连续调用该方法,则第二次调用将返回false。在api文档中说明interrupted()方法具有清除中断状态的功能。执行后具有将中断状态标识清除为false的功能。
- this.isInterrupted():检查线程是否已经中断,但是不能清除状态标识。
public static void main(String[] args) {
Thread thread = new Thread() {
public void run() {
System.out.println("线程启动了");
while (!isInterrupted()) {
System.out.println(isInterrupted()); // 调用 interrupt 之后为true
}
System.out.println("线程结束了");
}
};
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
System.out.println("线程是否被中断:" + thread.isInterrupted());//true
}
5.4 总结
来一个综合的例子:
public class test1 {
static volatile boolean flag = true;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("开始休眠");
try {
Thread.sleep(100 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("结束休眠,开始死循环");
while (flag) {
}
System.out.println("------------------子线程结束------------------");
}
});
thread.start();
Scanner scanner = new Scanner(System.in);
System.out.println("输入1抛出一个中断异常,输入2修改循环标志位,输入3判断线程是否阻塞,输入其他结束Scanner\n");
while (scanner.hasNext()) {
String text = scanner.next();
System.out.println("你输入了:" + text + "\n");
if ("1".equals(text)) {
thread.interrupt();
} else if ("2".equals(text)) {
flag = false; //如果不设为false,主线程结束后子线程仍在运行
} else if ("3".equals(text)) {
System.out.println(thread.isInterrupted());
} else {
scanner.close();
break;
}
}
System.out.println("------------------主线程结束------------------");
}
}
5.5 常见模式——两阶段中止模式
这里提供针对interrupt中断使用的一种常见模式,方便大家在后续的开发过程中使用。
5.5.1 使用场景
在一个线程中“优雅”地停止另一个线程的工作,在停止的时候给另一个线程"料理后事"的机会。比如有个监控线程在做监控,在主线程可以终止他的工作。
5.5.2 代码展示
MonitorTask类:
class MonitorTask {
private Thread thread;
// 开始监控
public void start() {
thread = new Thread(() -> {
while (true) {
Thread curr = thread.currentThread();
// 如果当前线程是中断状态,才会执行料理后事地逻辑
if(curr.isInterrupted()) {
log.debug("清理资源,料理后事");
break;
}
try {
// 监控执行工作...
log.debug("监控执行ing");
// 如果调用了该线程的中断方法,就会马上触发sleep抛出异常,终止掉监控工作
Thread.sleep(1000);
} catch (InterruptedException e) {
// 处理异常
log.error("error", e);
// 此处中断位被清除,为false, 调用中断方法重新改为true,再次进入循环,处理线程后事
curr.interrupt();
}
}
}, "监控线程");
thread.start();
}
// 终止线程
public void stop() {
thread.interrupt();
}
}
main方法中的代码:
MonitorTask monitorTask = new MonitorTask();
monitorTask.start();
Thread.sleep(1500);
monitorTask.stop();
执行结果:
相关文章: 【并发基础】线程的通知与等待:obj.wait()、obj.notify()、obj.notifyAll()详解
【并发基础】join()方法底层原理详解
【并发基础】一篇文章带你彻底搞懂睡眠、阻塞、挂起、终止之间的区别
参考链接: https://www.cnblogs.com/jojop/p/13957027.html
https://blog.csdn.net/anlian523/article/details/106752414/