目录
4.2 调用LockSupport.park()与LockSupport.unpark()
4.3 interrupt()与LockSupport.park()
4.4 interrupt()与Thread.sleep()
5.3.1 使用 interrupt() + InterruptedException来中断线程
5.3.2 使用 interrupt() + isInterrupted()来中断线程
〇、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/