前言
在Java中,线程中断是一种机制,用于通知线程应该停止当前正在执行的任务。中断通常用于协同线程之间的合作,以便让线程在适当的时候终止其工作,尤其是在长时间运行的任务或阻塞操作中。通过学了多线程以及synchronized的相关知识,接下来就到了学习线程中断知识。
中断机制
在Java中,中断机制的概念被引入到多线程编程中,用于线程间的协作和通信。线程的中断机制使得线程可以在不中止其执行的情况下,通过发送中断信号来通知线程应该停止当前的工作或改变其执行流程。
尽管在Java中Thread.stop()、Thread.suspend() 和 Thread.resume()三个API,但是线程的中断/停止应该是由线程本身来停止,所以这些也已经被废弃了。
而且Java中没有办法来立即停止一条线程,但是又需要停止线程,Java提供了一种协商机制:中断。
三大中断方法
在Java中,线程中断(Thread Interruption)是一种用于请求线程停止其当前任务或改变执行状态的机制。它不是强制性的终止,而是通过设置一个“中断标志”来提示目标线程可以停止执行,具体需要目标线程自行检查该标志并作出响应。Java也提供了许多API,但其中有三个中断方法是主要的。
这三个API分别是 interrupt()
、isInterrupted()
和 interrupted()
,接下来我们也将仔细学习这三个主要的API。
interrupt
具体API - 实例方法 - public void interrupt()
请求线程中断,仅仅只是将线程的中断标志设置为true
,发起一个协商但并不是立即停止线程。
使用场景:常用于从外部中断线程,提示该线程可以停止当前工作。
java
代码解读
复制代码
Thread thread = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { // 执行任务 } }); thread.start(); thread.interrupt(); // 请求中断该线程
interrupted
具体API - 静态方法 - public static boolean interrupted()
静态方法,检查当前线程的中断状态,并清除中断标志。第二次调用该方法会返回 false,因为中断标志已经被清除。
这里需要注意的是:
- 如果线程在wait(),wait(long)调用阻塞,或wait(long, int) Object类的方法,或者对join(),join(long),join(long, int),sleep(long),或sleep(long, int),这类方法,那么它的中断状态将被清除,它会收到InterruptedException异常。
- 如果该线程被阻塞在I/O操作在一个InterruptibleChannel然后通道将被关闭,该线程的中断状态将被设置,并且线程将获得ClosedByInterruptException。
- 如果该线程是在一个Selector然后该线程的中断状态将被设置,它会立即从选择操作回流受阻,可能是一个非零的值,就像选择器的wakeup方法调用。
- 如果前面的条件没有保持,那么这个线程的中断状态将被设置。
- 中断一个不活跃的线程不会有任何效果。
换句话说,如果这个方法被连续调用两次,那么第二个调用将返回false(除非当前线程再次中断,在第一个调用已经清除其中断状态之后,在第二个调用之前已经检查过)。 忽略线程中断,因为线程在中断时不存在将被该方法返回false所反映。
使用场景:当需要检查并同时重置中断状态时使用。
java
代码解读
复制代码
if (Thread.interrupted()) { // 处理中断情况,并且清除中断状态 }
isInterrupted
具体API - 实例方法 - public boolean isInterrupted()
检查当前线程是否被中断。返回 true 表示线程已经被请求中断,但不会清除中断状态。
使用场景:一般是用来定期检查线程的中断标志,以决定是否需要提前结束任务。
java
代码解读
复制代码
if (Thread.currentThread().isInterrupted()) { // 线程已经被请求中断,做一些清理或退出操作 }
通过以下简单的demo来了解一下以上的API
java
代码解读
复制代码
public class InterruptDemo { public static void main(String[] args) throws InterruptedException { Thread taskThread = new Thread(() -> { try { while (!Thread.currentThread().isInterrupted()) { System.out.println("任务执行中..."); Thread.sleep(2000); // 模拟长时间操作 } } catch (InterruptedException e) { System.out.println("线程被中断!"); } }); taskThread.start(); Thread.sleep(5000); // 主线程等待5秒后中断任务线程 taskThread.interrupt(); // 请求中断 } }
通过运行,我们可以看到,在线程启动后,如果没有检测到请求中断,那么就会一直执行,直到请求中断,JVM会将中断标记位设置为true,此时检测到就会执行中断业务。
Java线程中断机制是通过设置和检查中断标志来实现的,线程本身需要定期检查标志来响应中断请求,而不是直接被强制终止。通过API interrupt()、isInterrupted() 和 interrupted(),我们可以灵活地管理线程的中断行为。
中断线程的实现
如何来停止中断运行中的线程?其实也有许多方法,只要能够提供一个具有可见性的变量,就能够做到中断线程,因为我们上文提到了,中断线程应该是由线程自己来中断,我们可以通过判断某个值是否达到标记的值,是的话就执行中断,当然也可以使用中断的API,道理都是差不多的。接下来就来看看例子。
volatile实现
关于volatile可以看一下之前的文章《【多线程与高并发】- 浅谈volatile》。这篇文章对volatile做了详细讲解,这里我们就需要清楚一点是volatile的一个可见性的特性,当我们某个线程对加了此修饰的变量修改了,其他线程是能够感知的到的。
我们通过一个例子来解释,首先定义一个用volatile
修饰的变量,其次在执行线程t1的时候,会进入一段循环,来实现线程t1在不断地执行,如果flag被修改为true,则退出循环,此线程也就停止。接着通过睡眠10毫秒,启动另一个线程t2来进行修改flag。
整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取
shell
代码解读
复制代码
/** * 中断demo * @Author: lyd * @Date: 2024/9/7 16:55 */ public class InterruptDemo { static volatile boolean flag = false; public static void main(String[] args) throws InterruptedException { new Thread(() -> { // 假设开启线程后循环执行 while (true) { if (flag) { System.out.println("volatile 修饰的变量被修改为 true,线程t1结束!"); break; } System.out.println("线程 t1 正在执行中...."); } }, "t1").start(); // 假设过了一整子,flag被修改了 Thread.sleep(10); new Thread(() -> flag = true, "t2").start(); } }
通过运行结果,我们得以看出,实现了线程的中断,这是因为volatile修饰的变量是可见的。
AtomicBoolean实现
AtomicBoolean 是 Java 中位于java.util.concurrent.atomic
包中的一个类,具有原子性的特性,它提供了一种通过原子操作来对布尔值进行修改的机制。使用 AtomicBoolean,你可以确保在多线程环境下对布尔变量的操作是线程安全的,而无需使用显式的同步(如 synchronized 关键字)。
简单了解一下:
AtomicBoolean 通过底层的硬件支持(如 CAS 操作,比较并交换)来保证对其内部布尔值的操作是原子的。它提供了几个非常有用的方法,用于无锁地修改布尔值,并确保多个线程能够正确且安全地进行并发访问。
通过以下代码,与使用volatile的类似,只不过这次是使用了AtomicBoolean来实现。
shell
代码解读
复制代码
/** * @Author: lyd * @Date: 2024/9/7 20:25 */ public class InterruptDemo_atomic { static AtomicBoolean flag = new AtomicBoolean(false); public static void main(String[] args) throws InterruptedException { new Thread(() -> { // 假设开启线程后循环执行 while (true) { if (flag.get()) { System.out.println("AtomicBoolean 类型的变量被修改为 true,线程t1结束!"); break; } System.out.println("线程 t1 正在执行中...."); } }, "t1").start(); // 假设过了一整子,flag被修改了 Thread.sleep(10); new Thread(() -> flag.set(true), "t2").start(); } }
运行结果
使用Thread的中断API
通过上文介绍了interrupt()
、isInterrupted()
和 interrupted()
这三个API,我们要想中断某个线程,那就是发起一个协商机制,要让线程自己去中断。用以下例子来进行测试,首先在线程t1中通过isInterrupted()
来判断线程的中断标记是否被标记了,如果标记了,那就执行退出逻辑,接着在t2线程中通过interrupt()
执行中断线程,它仅仅只是将t1线程的中断标记设置了true。
shell
代码解读
复制代码
/** * @Author: lyd * @Date: 2024/9/8 10:20 */ public class InterruptDemo_api { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { // 假设开启线程后循环执行 while (true) { if (Thread.currentThread().isInterrupted()) { System.out.println("isInterrupted API检测到中断标记为 true,线程t1结束!"); break; } System.out.println("线程 t1 正在执行中...."); } }, "t1"); t1.start(); Thread.sleep(10); new Thread(() -> t1.interrupt(), "t2").start(); } }
我们通过执行可以看出效果
中断原理
接下来通过源码来理解一下中断的实现过程。
Thread.interrupt()源码解析
Thread.interrupt()
方法是通知线程它应该停止执行的一种方式。这个方法会设置线程的“中断标志”,使得线程可以检测到这一状态。
java
代码解读
复制代码
public void interrupt() { if (this != Thread.currentThread()) checkAccess(); synchronized (blockerLock) { Interruptible b = blocker; if (b != null) { interrupt0(); // 设置本地中断标志,并唤醒被阻塞的线程 b.interrupt(this); return; } } interrupt0(); // 设置线程的中断标志 }
从源码我们看出实际上这个方法会执行interrupt0(),并且仅仅只是设置中断标记。
这个interrupt0()
是个本地方法,它依赖 JVM 内部机制来处理中断请求。它主要做了两件事情:
- 设置当前线程的中断标志。
- 如果线程处于阻塞状态(如 sleep()、wait() 或 I/O 操作),则使线程从阻塞状态唤醒。
java
代码解读
复制代码
private native void interrupt0();
在 JVM 层面,interrupt0() 是与操作系统相关的实现,它会设置线程的内部状态来标记这个线程已经被中断,同时如果线程正在进行 I/O 等阻塞操作,会通过特定的机制将线程唤醒。
检测中断状态
线程可以通过以下方式检测自身是否已经被中断:
Thread.interrupted()
:检查当前线程是否已经被中断,并清除中断状态。Thread.isInterrupted()
:检查线程是否被中断,但不会清除中断状态。
java
代码解读
复制代码
if (Thread.interrupted()) { // 处理中断逻辑 }
但是,在 Java 中,某些阻塞方法(例如 Thread.sleep()、Object.wait()、Thread.join() 等)会自动处理中断并抛出 InterruptedException。
java
代码解读
复制代码
public static void sleep(long millis) throws InterruptedException { if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { return; } long start = System.currentTimeMillis(); long timeLeft = millis; while (timeLeft > 0) { try { Thread.sleep(timeLeft); } catch (InterruptedException e) { throw e; // 线程被中断,抛出异常 } timeLeft = millis - (System.currentTimeMillis() - start); } }
当线程处于 sleep()
或 wait()
状态时,如果线程被中断,它将抛出 InterruptedException
,线程可以捕获这个异常并处理相应的逻辑。
特殊案例
具体我们通过以下案例来进行具体学习。
正常情况
我们启动一个线程,在线程内部进行循环打印,等for循环执行完毕后输出t1的中断标志位信息。线程启动后会输出执行interrupt
前的中断标志,接着延时2ms在打印线程的中断标志信息。
java
代码解读
复制代码
public class InterruptDemo2 { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i <= 300; i++) { System.out.println("执行中..." + i); } System.out.println("中断后的t1线程的中断标志 - 2 : " + Thread.currentThread().isInterrupted()); }, "t1"); t1.start(); System.out.println("中断前的t1线程的中断标志: " + t1.isInterrupted()); Thread.sleep(2); t1.interrupt(); // 请求中断 System.out.println("中断后的t1线程的中断标志 - 1 : " + t1.isInterrupted()); } }
这个案例的目的就是为了让我们直观的了解线程的中断标志的变化以及中断标志的变化与线程中断的联系。以上代码的输出结果如下:
java
代码解读
复制代码
中断前的t1线程的中断标志: false 执行中...0 // ... 执行中...189 中断后的t1线程的中断标志 - 1 : true 执行中...190 // ... 执行中...297 执行中...298 执行中...299 执行中...300 中断后的t1线程的中断标志 - 2 : true
从上面结果我们可以看出,虽然调用了interrupt(),但是并不会使线程直接中断,它只是做了个标记而已。
线程外延迟查看
我们再来看下面这个例子,我们在后面进行睡眠了2s,主要是为了让线程300次循环执行结束,再来打印当前线程的中断标志数据。
java
代码解读
复制代码
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i <= 300; i++) { System.out.println("执行中..." + i); } System.out.println("中断后的t1线程的中断标志 - 2 : " + Thread.currentThread().isInterrupted()); }, "t1"); t1.start(); System.out.println("中断前的t1线程的中断标志: " + t1.isInterrupted()); Thread.sleep(2); t1.interrupt(); // 请求中断 System.out.println("中断后的t1线程的中断标志 - 1 : " + t1.isInterrupted()); Thread.sleep(2000); System.out.println("中断后的t1线程的中断标志 - 3 : " + t1.isInterrupted()); }
运行结果如下
java
代码解读
复制代码
中断前的t1线程的中断标志: false 执行中...0 // ... 执行中...226 中断后的t1线程的中断标志 - 1 : true 执行中...227 执行中...228 // ... 执行中...299 执行中...300 中断后的t1线程的中断标志 - 2 : true 中断后的t1线程的中断标志 - 3 : false
为什么已经调用了interrupt(),但是在最后一次打印的时候却是输出了false?在程序睡眠2秒结束后,实际上,这个线程已经执行完毕了,也就是中断一个不活跃的线程不会有任何效果。所以这里输出的是false。
线程内睡眠
首先,先来看以下正常的例子,按正常来说,我们应该是在t1线程内自己去根据中断标记的值来进行自行中断线程,而t2线程(其他线程)也就只是发起协商中断,将中断标识设置了true。
java
代码解读
复制代码
public class InterruptDemo3 { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { // 假设开启线程后循环执行 while (true) { if (Thread.currentThread().isInterrupted()) { System.out.println("isInterrupted API检测到中断标记为 true,线程t1结束!"); break; } System.out.println("线程 t1 正在执行中...."); } }, "t1"); t1.start(); // 假设过了1s,flag被修改了 Thread.sleep(1000); new Thread(() -> t1.interrupt(), "t2").start(); } }
显然这个案例是会正常的执行退出。
java
代码解读
复制代码
线程 t1 正在执行中.... 线程 t1 正在执行中.... // ... 线程 t1 正在执行中.... isInterrupted API检测到中断标记为 true,线程t1结束!
那么我们在线程t1内部的循环加个睡眠3s
java
代码解读
复制代码
public class InterruptDemo3 { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { // 假设开启线程后循环执行 while (true) { if (Thread.currentThread().isInterrupted()) { System.out.println("isInterrupted API检测到中断标记为 true,线程t1结束!"); break; } try { Thread.sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("线程 t1 正在执行中...."); } }, "t1"); t1.start(); // 假设过了1s,flag被修改了 Thread.sleep(1000); new Thread(() -> t1.interrupt(), "t2").start(); } }
这个时候,我们模拟出来的是,在t2线程向t1发起协商中断的时候,由于t1是处于睡眠,那么就会抛出InterruptedException
异常。
也就是说当调用interrupt()的时候,如果遇到了sleep(具体看上文关于interrupt API的介绍,也可以看一下JDK的API文档),就会抛出异常。
- interrupt()方法: 这是一个中断线程的机制。它不会直接终止目标线程,而是设置线程的中断标志,通知线程发生了中断,线程可以根据这个通知来处理中断的情况。
- 阻塞状态: 当线程调用像
sleep()
、wait()
、join()
等方法时,会进入阻塞状态,等待某个条件完成。如果此时其他线程对其调用interrupt()
,Java 的线程管理机制会主动检查线程的状态,发现目标线程正处于阻塞中,这时会抛出InterruptedException
以提醒线程可以停止阻塞并处理中断情况。- 设计目的: 抛出
InterruptedException
的目的是给线程提供一种立即响应中断的方式,使得阻塞操作不会无限期等待。例如,sleep()
是一种不确定的等待,而抛出InterruptedException
可以让线程在需要中断时,迅速恢复控制权,以便做出相应的处理(例如,退出或重新尝试操作)。
总结
Java 的中断机制并不是强制终止线程,而是通过设置中断标志、抛出 InterruptedException 来提示线程停止执行。线程必须自己响应和处理这些中断请求。Thread.interrupt() 方法和 interrupt0() 本地方法负责设置中断状态,而线程可以通过 Thread.interrupted() 或 Thread.isInterrupted() 来检测和响应这些状态。中断机制提供了灵活性,使得开发者能够在合适的时间点对线程进行停止或调整,避免了粗暴的线程终止。