理解Java线程中断

我们知道lock和synchronized在同步运用上的显著区别之一就是lock支持可中断,而synchronized不支持可中断。那么可中断实际上是怎么实现的,或者说我们怎么去理解中断这个概念。在lock的实现ReetrantLock中其实我们可以看到很多中断的运用。我们都知道死锁,作为一种独占的互斥锁,通过中断我们可以保证线程在阻塞的过程中可以响应中断,也就是结束当前线程对cpu的占用,通过向上层抛出异常来结束当前线程。

ReetrantLock中断的应用

我们看到ReetrantLock是如何实现中断的,这里主要看到三种中断的运用:

selfInterrupt()

ReetrantLock提供了lock和lockIntteruptibly两种上锁方式,那么他们有什么本质的区别,我们看到lock的一段源码

1
2
3
4
5
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

我们分析这段代码,lock默认非公平锁的情况下在tryAcquire(arg)时并不会排队,如果获取不到会去 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)进行排队,如果该方法返回ture则进入线程自我中断。那么我们可以看下这段代码是如何实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())//这里我们检验了中断标志是否被改变通过currentThread().isInterrupted(true)方法
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

这里的interrupted是我们定义的一个中断变量,可以看到这只是一个临时变量。当我们在队列中循环cas的时候,如果没有收到中断请求,那么最终会在第一组判断内进入并获取到锁,这是的中断标志为false,不中断,那么线程获取到锁之后开始执行同步代码块。但是如果在循环cas获取锁的过程中感知到了中断,中断的标志会被置为true,然后直接返回上层执行selfInterrupt(),我们再看下selfInterrupt()执行了什么操作:

1
2
3
4
5
6
/**
* Convenience method to interrupt current thread.
*/
static void selfInterrupt() {
Thread.currentThread().interrupt();//当前线程执行自我中断
}

总结:我们可以在获取锁的过程中记录一个临时变量,如果检测到当前执行线程被别的线程进行了某中断操作。将该临时变量置为true然后触发线程的自我中断。

抛出InterruptedException()异常

线程在检测到被中断的请求时,不会立即响应这个中断请求,因为它仅仅是改变了中断标示位,但是并不会直接触发中断,是否进行中断还是看当前线程什么时候去检测这个中断标示位。一般在阻塞锁的设计中,我们在循环等待的过程中会进行中断标志位的检测,从而进行InterruptedException()异常的抛出。这个中断异常的过程就好比,你妈妈会交代你好好吃饭,但是具体你是不是要好好吃饭,还是得由你自己决定,也就是我们在设计一个线程的run方法时,同样可以自定义一个volatile类型的中断标志位,来同步记录这个系统的标志位,然后在我们想要抛出异常的时候来检测这个标志位的变化,从而抛出相应的异常。这里看一段lockInterruptibly中的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Acquires in exclusive interruptible mode.
* @param arg the acquire argument
*/
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 (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())//检测到中断标示位被修改为true的时候直接向上层抛出异常。
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

中断的相关方法

这里记录一下几个关键的调用中断的方法:

  • public void interrupt()
    将调用者线程的中断状态设为true。
  • public boolean isInterrupted()
    判断调用者线程的中断状态。
  • public static boolean interrupted
    只能通过Thread.interrupted()调用。
    它会做两步操作:返回当前线程的中断状态同时将当前线程的中断状态设为false。

线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以通过调用静态方法Thread.interrupted()对当前线程的中断标示位进行复位。如果该线程已经处于终结状态,那么即使该线程被中断过,在调用该线程该线程对象的isInterrrupted()时会返回false。

我们查看java的api可以看到,很多会抛出中断异常的方法都会在抛出异常之前,java虚拟机首先将该线程的中断标示位清除。那么此时我们调用isInterrrupted()时会返回false。

中断异常代码示例

我们创建两个线程sleepThread和busyThread,第一个不停的执行休眠,第二个不停的执行循环代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package Interrupted;

/**
* Created by yqz on 3/22/18.
*/
public class Interrupted {
public static void main(String[] args) {
//新建一个睡眠线程
Thread sleepThread=new Thread(new Runnable() {
@Override
public void run() {
while (true){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"sleepThread");
//新建一个忙碌的线程
Thread busyThread=new Thread(new Runnable() {
@Override
public void run() {
while (true){
}
}
},"busyThread");
sleepThread.start();
busyThread.start();
try {
Thread.sleep(10000);//线程休眠十秒
} catch (InterruptedException e) {
e.printStackTrace();
}
sleepThread.interrupt();
busyThread.interrupt();
System.out.println("sleepThread的中断标志为:"+sleepThread.isInterrupted());
System.out.println("busyThread的中断标志为:"+busyThread.isInterrupted());
}
}

输出的结果为:

1
2
3
4
5
6
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at Interrupted.Interrupted$1.run(Interrupted.java:14)
at java.lang.Thread.run(Thread.java:745)
sleepThread的中断标志为:false
busyThread的中断标志为:true

可以看到,抛出异常的线程的中断标示位被清除了,但是一直忙碌的线程因为并没有去判断中断标示位,所以即使中断标示位已经是true也不会执行中断操作。

总结

线程中断是一种是可以理解为一个线程对另一个线程做出了一个标示位的改变,通过触发线程中断会将中断标示位置为true,被中断的线程可以选择忽略这个标志,也可以选择在某个时刻校验标志位抛出异常,同时将标志位清除。