只是不愿随波逐流 ...|

lidongdongdong~

园龄:2年7个月粉丝:14关注:8

52、线程中断

内容来自王争 Java 编程之美

大部分情况下线程在执行完业务逻辑之后便自行结束,但是少数情况下,由于应用程序关闭等原因,线程在执行业务逻辑的过程中有可能提前被终止
我们需要寻找一些安全的线程终止方式,避免突然中止业务逻辑而导致的数据不一致、资源得不到回收等问题
本节我们就详细讲一讲,如何安全地提前终止线程,并且重点讲解其中的中断方法

1、基于标志终止线程

基于标志终止线程是一种比较常用的终止线程的方法,如下示例所示
线程 t1 是一个长时间执行的程序,在执行的过程中会检查 stopped 是否为 true
如果想终止线程 t1,我们只需要在另一个线程中,比如在下述代码中的线程 main 中,将 stopped 设置为 true 即可

public class Demo {
private static volatile boolean stopped = false;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
int count = 0;
while (!stopped) {
System.out.println(count++);
}
}
});
t1.start();
Thread.sleep(1000); // 1 s
stopped = true;
}
}

2、基于中断终止线程

2.1、介绍

除了基于标志来终止线程,我们还可以基于中断来终止线程
基于标志来终止线程,我们需要自己定义标志变量,而基于中断来终止线程,我们直接使用线程提供的内部中断标志位,无须自己定义
线程提供了如下 3 个函数来操作中断标志位

// 位于 Thread.java 中
public void interrupt(); // 设置中断标志位
public boolean isInterrupted(); // 检查中断标志位是否设置
public static boolean interrupted(); // 检查并清除中断标志位

2.2、示例 1

使用基于中断终止线程的方式,我们对上述示例重新实现,如下代码所示
实际上两种终止线程的方式在本质上是一样的,都是基于标志位,只不过标志位定义的位置不同,一个在业务代码中,一个在 Thread 线程中

public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted()) {
System.out.println(count++);
}
}
});
t1.start();
Thread.sleep(1000); // 1 s
t1.interrupt();
}
}

2.3、示例 2

从上述示例代码我们还可以发现,线程 main 调用 interrupt() 函数,只不过是设置了线程 t1 的中断标志位,相当于只是发起了中断请求,并非真正将线程 t1 中断
线程 t1 既可以像上述示例代码那样响应中断,也可以像如下示例代码那样无视中断,具体如何处理中断要看具体的业务需求
也就是说:Java 提供的中断是一种协作机制,发起中断的线程和被中断的线程需要互相协作,才能达到终止线程的目的
这种非强制的中断机制,可以让被中断的线程有时间进行终止前的善后工作,以此来避免数据不一致或资源无法回收等问题的发生

public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
int count = 0;
// 无视中断
while (true) {
System.out.println(count++);
}
}
});
t1.start();
Thread.sleep(1000); // 1 s
t1.interrupt(); // 发起中断请求
}
}

3、基于中断异常终止线程

3.1、介绍

我们知道,阻塞函数会阻塞当前线程,而且可能长时间阻塞,比如 Thread.sleep(),如果在线程执行的代码中包含对阻塞函数的调用
那么当我们通过 interrupt() 向这个线程发起中断请求时,如果线程正在执行阻塞函数,那么线程将无法响应中断请求,也就无法及时终止线程,对于这个问题,该如何解决呢?

实际上大部分阻塞函数在设计实现时都已经考虑到了这个问题
当这些阻塞函数接受到中断请求之后,会停止执行并抛出 InterruptedException 中断异常,我们可以基于中断异常来终止线程,示例代码如下所示

3.2、示例

需要注意的是,大部分阻塞函数在抛出 InterruptedException 之前,会调用 interrupted() 函数清除中断标志位
因此在如下代码中,当代码 catch 到 InterruptedException 之后,需要重新使用 interrupt() 设置线程的中断标志位
当然对于以下代码逻辑,我们也可以在 catch 到 InterruptedException 之后,直接调用 return 语句来终止线程

public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted()) {
System.out.println(count++);
try {
Thread.sleep(10000); // 10 s
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 重新设置线程的中断标志位
}
}
}
});
t1.start();
Thread.sleep(2000); // 2 s
t1.interrupt();
}
}

3.3、更多支持中断的阻塞函数

实际上除了 Thread.sleep() 之外,前面讲到的 Lock、Condition、Semaphore 等也提供了很多支持中断的阻塞函数,它们也同时提供了对应的不支持中断的阻塞函数,如下所示

// Lock 接口
void lock(); // 不可中断
void lockInterruptibly() throws InterruptedException; // 可中断
// Condition 类
void await() throws InterruptedException; // 可中断
void awaitUninterruptibly(); // 不可中断
// Semaphore 类
public void acquire() throws InterruptedException; // 可中断
public void acquireUninterruptibly(); // 不可中断

可中断的阻塞函数在接收到中断请求之后,会终止执行并抛出 InterruptedException 异常,不可中断的阻塞函数在接收到中断请求之后,不做任何处理

3.4、Lock 接口源码

我们拿 Lock 接口的两个函数举例,结合源码剖析:可中断阻塞函数和不可中断阻塞函数的底层实现原理

1、不可中断阻塞函数 lock() 的实现原理
lock() -> acquire() -> tryAcquire() -> addWaiter() -> acquireQueued()

lock() 函数直接调用 AQS 中的 acquire() 函数,acquire() 函数的代码实现如下所示
acquire() 函数调用 tryAcquire() 函数尝试竞争获取锁,如果获取失败,则执行 acquireQueued() 函数排队等待

acquireQueued() 函数返回 true 表示在函数执行过程中收到了中断请求
但为了避免中断干扰,在函数内部已经清除了中断标志位的设置,因此在 acquireQueued() 函数执行结束之后,acquire() 函数调用 selfInterrupt() 函数重新设置中断标志位

// AbstractQueueSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}

acquireQueued() 函数的代码实现如下所示,对于 acquireQueued() 函数的代码实现逻辑,我们已经在讲解 AQS 时详细讲解过了,我们重点看下其中的中断处理逻辑

在 acquireQueued() 函数中,parkAndCheckInterrupt() 函数为阻塞函数,底层调用 LockSupport.park(),进而再调用 Unsafe.park()
导致 parkAndCheckInterrupt() 函数返回的情况有两种:被其他线程唤醒(调用 AQS 中的 release() 函数)和被其他线程中断(调用 interrupt() 函数)
如果 parkAndCheckInterrupt() 函数是被中断返回,那么 acquireQueued() 函数会 for 循环再次执行 parkAndCheckInterrupt() 函数
也就是说,中断请求并不会终止 acquireQueued() 函数的执行

// 位于 AQS 类
// 返回值为 true 表示在函数处理期间, 线程被中断过
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (; ; ) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed) cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted(); // 检查并清除中断设置
}
// 位于 LockSupport 类
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}

这里我们稍微解释一下上述代码中的 Unsafe.park() 函数
前面我们提到,在 Linux 操作系统下,Unsafe.park() 底层是采用 pthread 库的 pthread_cond_wait() 条件变量来实现,其中并不包含中断标志位的检测逻辑
那么当另一个线程调用 interrupt() 设置中断标志位之后,Unsafe.park() 是如何退出阻塞等待的呢?
实际上答案很简单,那就是:interrupt() 的底层实现不仅仅会设置中断标志位,还会执行类似 Unsafe.unpark() 函数一样的逻辑

2、可中断阻塞函数 lockInterruptibly() 的实现原理

lockInterrupteibly() 直接调用 AQS 中的 acquireInterruptibly() 函数,acquireInterruptibly() 函数的代码实现如下所示
acquireInterruptibly() 接收到中断请求之后,会终止函数的执行并抛出 InterruptedException

public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted()) throw new InterruptedException();
if (!tryAcquire(arg)) doAcquireInterruptibly(arg);
}

我们重点看下 doAcquireInterruptibly() 函数,其代码实现如下所示
doAcquireInterruptibly() 函数的核心处理逻辑跟前面讲到的 acquireQueued() 函数的基本一致,区别主要在于对中断的响应方式
在 doAcquireInterruptibly() 函数中,当 parkAndCheckInterrupt() 函数因为中断而返回时,直接抛出 InterruptedException

private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (; ; ) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed) cancelAcquire(node);
}
}

4、Java 中断 VS 操作系统中断

我们知道操作系统中也会有中断,那么本节所讲的 Java 中断和操作系统中断有什么区别和联系呢?
实际上 Java 中断和操作系统中断是两回事,只不过从功能上来讲,它们都是用来打断某个正在执行的任务而已
从上述对 Java 中断的讲解,我们可以发现,Java 中断的实现完全由 Java 语言独立实现,并不依赖操作系统中断

Java 中断用来中断线程,操作系统中断是用来中断 CPU,CPU 在执行指令的过程中,每当一个 CPU 周期执行完成之后,就会去中断寄存器中检查是否有中断请求
如果有中断请求,则根据中断请求编号,在事先设置好的中断向量表中,查找对应的中断处理程序入口地址,然后跳转去执行对应的中断处理程序
常用的操作系统中断有:I / O 中断(响应鼠标、键盘、磁盘等 I / O 设备的输入)、时钟中断、异常、系统调用中断等等
有关操作系统中断更详细的讲解,你可以查阅操作系统相关的书籍

5、课后思考题

细心的你应该已经发现,在本节中给出的 Lock 接口的两个函数的命名方式,跟 Condition、Semaphore 类中的函数的命名方式不同
在 Lock 接口中,命名比较短的函数表示不可中断函数,而在 Condition、Semaphore 类中,命名比较短的函数表示可中断函数
那么命名方式不一致是故意为之还是随意为之呢?

// Lock 接口
void lock(); // 不可中断
void lockInterruptibly() throws InterruptedException; // 可中断
// Condition 类
void await() throws InterruptedException; // 可中断
void awaitUninterruptibly(); // 不可中断
// Semaphore 类
public void acquire() throws InterruptedException; // 可中断
public void acquireUninterruptibly(); // 不可中断

答案:用较短的单词命名常用函数方便代码编写

posted @   lidongdongdong~  阅读(36)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
展开