并发编程-线程中断

中断标志就是线程对象的一个成员变量,它表示一个运行中的线程是否被被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程对象的interrupt()方法对其进行中断操作。

线程通过检查自身是否被中断来进行响应,线程通过对象方法 isInterrupted()来进行判断是否被中断。

在线程中调用静态方法 Thread.interrupted() 对当前线程的中断标识进行复位。

中断与响应中断是异步的。

本文将从以下两个方面来介绍Java中对线程中断机制的具体实现:

  1. Java中对线程中断所提供的API支持
  2. 线程在不同状态下对于中断所产生的反应

1. Java中线程中断的API

在以前的jdk版本中,我们使用stop方法中断线程,但是现在的jdk版本中已经不再推荐使用该方法了,反而由以下三个方法完成对线程中断的支持。

1
2
3
4
5
public boolean isInterrupted()

public void interrupt()

public static boolean interrupted()

每个线程都一个状态位用于标识当前线程对象是否是中断状态。isInterrupted是一个实例方法,主要用于判断当前线程对象的中断标志位是否被标记了,如果被标记了则返回true表示当前已经被中断,否则返回false。我们也可以看看它的实现源码:

1
2
3
public boolean isInterrupted() {
return isInterrupted(false);
}

1
private native boolean isInterrupted(boolean ClearInterrupted);

底层调用的本地方法isInterrupted,传入一个boolean类型的参数,用于指定调用该方法之后是否需要清除该线程对象的中断标识位。由于参数值是false,所以调用isInterrupted并不会清除线程对象的中断标识位。

interrupt也是一个实例方法,该方法用于设置线程对象的中断标识位,只要能获取到线程对象,就能调用该方法。

interrupted()是一个静态的方法,只能在线程内部执行中调用,用于返回当前线程执行的线程是否被外部中断了,返回后清空标志位

1
2
3
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
1
private native boolean isInterrupted(boolean ClearInterrupted);

该方法用于判断当前线程是否被中断,并且该方法调用结束的时候会清空中断标识位。

下面我们看看线程所处不同状态下对于中断操作的反应。

2. 线程在不同状态下对于中断所产生的反应

线程一共6种状态,分别是NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED(Thread类中有一个State枚举类型列举了线程的所有状态)。下面我们就将把线程分别置于上述的不同种状态,然后看看我们的中断操作对它们的影响。

2.1 NEW和TERMINATED

线程的new状态表示还未调用start方法,还未真正启动。线程的terminated状态表示线程已经运行终止。这两个状态下调用中断方法来中断线程的时候,Java认为毫无意义,所以并不会设置线程的中断标识位,什么事也不会发生。例如:

1
2
3
4
5
6
public static void main(String[] args) throws InterruptedException {
Thread thread = new MyThread();
System.out.println(thread.getState());
thread.interrupt();
System.out.println(thread.isInterrupted());
}

输出结果如下:

1
2
NEW
fales

terminated状态:

1
2
3
4
5
6
7
8
public static void main(String[] args) throws InterruptedException {
Thread thread = new MyThread();
thread.start();
thread.join();
System.out.println(thread.getState());
thread.interrupt();
System.out.println(thread.isInterrupted());
}

输出结果如下:

1
2
TERMINATED
false

从上述的两个例子来看,对于处于new和terminated状态的线程对于中断是屏蔽的,也就是说中断操作对这两种状态下的线程是无效的。

2.2 RUNNABLE

如果线程处于运行状态,那么该线程的状态就是RUNNABLE,但是不一定所有处于RUNNABLE状态的线程都能获得CPU运行,在某个时间段,只能由一个线程占用CPU,那么其余的线程虽然状态是RUNNABLE,但是都没有处于运行状态。而我们处于RUNNABLE状态的线程在遭遇中断操作的时候只会设置该线程的中断标志位,并不会让线程实际中断,想要发现本线程已经被要求中断了则需要用程序去判断。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyThread extends Thread{
@Override
public void run(){
while(true){
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new MyThread();
thread.start();
System.out.println(thread.getState());

thread.interrupt();
Thread.sleep(1000);//等到thread线程被中断之后
System.out.println(thread.isInterrupted());
System.out.println(thread.getState());
}

我们定义的线程始终循环做一些事情,主线程启动该线程并输出该线程的状态,然后调用中断方法中断该线程并再次输出该线程的状态。总的输出结果如下:

1
2
3
RUNNABLE
ture
RUNNABLE

可以看到在我们启动线程之后,线程状态变为RUNNABLE,中断之后输出中断标志,显然中断位已经被标记,但是当我们再次输出线程状态的时候发现,线程仍然处于RUNNABLE状态。很显然,处于RUNNBALE状态下的线程即便遇到中断操作,也只会设置中断标志位并不会实际中断线程运行。那么问题是,既然不能直接中断线程,我要中断标志有何用处?
这里其实Java将这种权力交给了我们的程序,Java给我们提供了一个中断标志位,我们的程序可以通过if判断中断标志位是否被设置来中断我们的程序而不是系统强制的中断。例如:

1
2
3
4
5
6
7
8
public void run(){
while(true){
if (Thread.currentThread().isInterrupted()){
System.out.println("exit MyThread");
break;
}
}
}

线程一旦发现自己的中断标志为被设置了,立马跳出死循环。这样的设计好处就在于给了我们程序更大的灵活性。

2.3 BLOCKED

当线程处于BLOCKED状态说明该线程由于竞争某个对象的锁失败而被挂在了该对象的阻塞队列上了。那么此时发起中断操作不会对该线程产生任何影响,依然只是设置中断标志位。例如:

1
2
3
4
5
6
7
8
9
10
11
public class MyThread extends Thread{
public synchronized static void doSomething(){
while(true){
//do something
}
}
@Override
public void run(){
doSomething();
}
}

这里我们自定义了一个线程类,run方法中主要就做一件事情,调用一个有锁的静态方法,该方法内部是一个死循环(占用该锁让其他线程阻塞)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new MyThread();
thread1.start();

Thread thread2 = new MyThread();
thread2.start();

Thread.sleep(1000);
System.out.println(thread1.getState());
System.out.println(thread2.getState());

thread2.interrupt();
System.out.println(thread2.isInterrupted());
System.out.println(thread2.getState());
}

在我们的主线程中,我们定义了两个线程并按照定义顺序启动他们,显然thread1启动后便占用MyThread类锁,此后thread2在获取锁的时候一定失败,自然被阻塞在阻塞队列上,而我们对thread2进行中断,输出结果如下:

1
2
3
4
RUNNABLE
BLOCKED
true
BLOCKED

从输出结果看来,thread2处于BLOCKED状态,执行中断操作之后,该线程仍然处于BLOCKED状态,但是中断标志位却已被修改。这种状态下的线程和处于RUNNABLE状态下的线程是类似的,给了我们程序更大的灵活性去判断和处理中断。

2.4 WAITING/TIMED_WAITING

这两种状态本质上是同一种状态,只不过TIMED_WAITING在等待一段时间后会自动释放自己,而WAITING则是无限期等待,需要其他线程调用notify方法释放自己。但是他们都是线程在运行的过程中由于缺少某些条件而被挂起在某个对象的等待队列上。当这些线程遇到中断操作的时候,会抛出一个InterruptedException异常,并清空中断标志位。例如:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyThread extends Thread{
@Override
public void run(){
synchronized (this){
try {
wait();
} catch (InterruptedException e) {
System.out.println("i am waiting but facing interruptexception now");
}
}
}
}

我们定义了一个线程类,其中run方法让当前线程阻塞到条件队列上,并且针对InterruptedException 进行捕获,如果遇到InterruptedException 异常则输出一行信息。

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws InterruptedException {
Thread thread = new MyThread();
thread.start();

Thread.sleep(500);
System.out.println(thread.getState());
thread.interrupt();
Thread.sleep(1000);
System.out.println(thread.isInterrupted());
}

在main线程中我们启动一个MyThread线程,然后对其进行中断操作。运行结果如下:

1
2
3
WAITING
i am waiting but facing interruptexception now
false

从运行结果看,当前程thread启动之后就被挂起到该线程对象的条件队列上,然后我们调用interrupt方法对该线程进行中断,输出了我们在catch中的输出语句,显然是捕获了InterruptedException异常,接着就看到该线程的中断标志位被清空。

3. 总结

综上所述,我们分别介绍了不同种线程的不同状态下对于中断请求的反应。

  • NEW 和 TERMINATED对于中断操作几乎是屏蔽的。
  • 对于RUNNABLE 和 BLOCKED状态,中断操作只是设置中断标志位并没有强制终止线程,对于线程的终止权利依然在程序手中,对于中断标志位的重置当然也是交给用户的。java将中断感知交给用户自己实现,即调用api进行判断。
  • 对于WAITING / TIMED_WAITING 状态下的线程,java牢牢控制着中断感知的权限,一旦被外部线程中断,会立即抛出异常并清空中断标志位。

对于2、3两点的总结:

实质上是中断感知异步同步的区别,第2点采用异步的方式感知中断,第三点采用同步的方式