之前的线程协作,讲的是通过wait和notify方法,多线程之间进行互相条件唤醒的办法。除此之外我们还需要进行中断操作。
等待和唤醒可参考前文https://www.cnblogs.com/jilodream/p/22770136
设想这样一个场景:长工在给地主家干活,日出而作,日落而息。
思路很简单: 长工线程的伪代码
1 public void run (){
2 while(true) {
3 if (time 早于 日出){
4 //继续休息
5 //continue;
6 }
7 if (time 早于 日落){
8 //继续工作
9 //continue;
10 }
11 //时间属于日落
12 //结束工作
13 //return;
14 }
15 }
这就是最典型的通过标志位状态来中断线程的操作。除了开发人员之外,java本身也给线程类内置了一个中断状态的标记变量。
/* Interrupt state of the thread - read/written directly by JVM */
private volatile boolean interrupted;
注释的意思:这是线程的中断状态,读写都是由JVM直接操纵的。
线程可以通过这个标记,来check自身的状态。并且针对这个变量,jdk提供了几个核心方法,供开发人员直接使用:
(1)中断这个线程,注意这是实例方法,所中断的线程也就是这个线程实例。
public void interrupt()
(2)查看这个线程的中断标记,注意这是实例方法,所查看的线程标记变量也就是这个线程实例内置的中断标记。
public boolean isInterrupted()
(3)查看线程的标记变量,注意这是一个静态方法,其所中断的线程也就是当前执行这个方法的线程currentThread。并且这个方法会清除标记,因此作者在命名时加了ed,表示是否过去被中断过。
public static boolean interrupted()
顺便依次看下源码:
1 public void interrupt() {
2 if (this != Thread.currentThread()) {
3 checkAccess();
4
5 // thread may be blocked in an I/O operation
6 synchronized (blockerLock) {
7 Interruptible b = blocker;
8 if (b != null) {
9 interrupted = true;
10 interrupt0(); // inform VM of interrupt
11 b.interrupt(this);
12 return;
13 }
14 }
15 }
16 interrupted = true;
17 // inform VM of interrupt
18 interrupt0();
19 }
20
21 private native void interrupt0();
这个方法的大致逻辑是:
先判断中断当前方法的线程是不是就是当前线程,
如果不是话,check一下安全设置,检查是否可以跨线程中断其它线程,如果有问题这里会抛出一个运行时异常 SecurityException。如果安全检查也没问题,判断当前线程是否处于I/O阻塞中。如果是的话就打断线程的I/O阻塞(阻塞的地方通常会抛出IOException相关的异常。
另外通常只有nio 的阻塞才会中断,传统的io不会响应中断,read write 方法依然会阻塞住),设置标记位interrupted为true,表示被中断了。并且调用native 方法interrupt0,让jvm来处理中断的具体操作。
如果是当前线程,或者没有处于阻塞中的话,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )这里直接设置标记位interrupted为true,并且调用native 方法interrupt0即可。
然后是获取中断状态,这里很简单,直接拿状态变量:
1 public boolean isInterrupted() {
2 return interrupted;
3 }
接着是获取并清理中断状态:
1 public static boolean interrupted() {
2 Thread t = currentThread();
3 boolean interrupted = t.interrupted;
4 // We may have been interrupted the moment after we read the field,
5 // so only clear the field if we saw that it was set and will return
6 // true; otherwise we could lose an interrupt.
7 if (interrupted) {
8 t.interrupted = false;
9 clearInterruptEvent();
10 }
11 return interrupted;
12 }
13
14 private static native void clearInterruptEvent();
核心逻辑是:取出当前线程t,以及当前线程的中断状态,如果当前线程的中断标记位true。那么就设置其为false,并且清理中断事件:告诉jvm,底层未处理的pending状态的中断信号清理掉。(后文红色字体会说到)
知道了这些api的作用,我们来看一个简单的例子:
1 public class InterruptStudy {
2 public static void main(String[] args) throws InterruptedException {
3
4
5 Thread t1 = new Thread(() -> {
6 while (!Thread.currentThread().isInterrupted()) {
7 System.out.println("do sth");
8 }
9 System.out.println("task end, interrupt state " + Thread.currentThread().isInterrupted());
10 return;
11 });
12
13 t1.start();
14 Thread.sleep(1000L);
15 t1.interrupt();
16 t1.join();
17 }
18
19 }
输出结果如下:
Connected to the target VM, address: '127.0.0.1:53895', transport: 'socket'
do sth
do sth
do sth
do sth
....
do sth
do sth
do sth
do sth
task end, interrupt state true
Disconnected from the target VM, address: '127.0.0.1:53895', transport: 'socket'
Process finished with exit code 0
这样子就可以通过中断标记,和线程进行交互从而中断线程了。
再改为使用静态方法:
1 public class InterruptStudy {
2 public static void main(String[] args) throws InterruptedException {
3
4
5 Thread t1 = new Thread(() -> {
6 while (!Thread.interrupted()) {
7 System.out.println("do sth");
8 }
9 System.out.println("task end, interrupt state " + Thread.currentThread().isInterrupted());
10 return;
11 });
12
13 t1.start();
14 Thread.sleep(10L);
15 t1.interrupt();
16 t1.join();
17 }
18
19 }
输出结果如下:
Connected to the target VM, address: '127.0.0.1:60872', transport: 'socket'
do sth
do sth
do sth
....
do sth
do sth
do sth
do sth
do sth
do sth
do sth
task end, interrupt state false
Disconnected from the target VM, address: '127.0.0.1:60872', transport: 'socket'
Process finished with exit code 0
发现中断以后,重置了中断标记,因此就返回false。
但是java的中断能力远不止此,如果仅仅是这样,那也太容易了。还记得上篇文章的wait么,以及更早之前的sleep,如果线程处于这种状态,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )无法自行的check 中断状态怎么办。没关系jvm 会自行的帮你唤醒,抛出一个InterruptedException。
同时在抛出异常之前,jvm还会帮你做两件事:清理标记,重新尝试获取锁。
请看下边这个例子:
1 public class InterruptExStudy {
2 private static final Object lock = new Object();
3
4 public static void main(String[] args) throws InterruptedException {
5
6
7 Thread t1 = new Thread(() -> {
8 synchronized (lock) {
9 System.out.println("task start");
10 try {
11 System.out.println("task do sth");
12 lock.wait();
13 } catch (InterruptedException e) {
14 System.out.println("task catch interrupt ex ,state: " + Thread.currentThread().isInterrupted());
15 }
16 System.out.println("task end ");
17 }
18 });
19
20 t1.start();
21 Thread.sleep(1000L);
22 synchronized (lock) {
23 System.out.println("before notify, t1 state-0 :" + t1.isInterrupted());
24 t1.interrupt();
25 System.out.println("before notify, t1 state-1 :" + t1.isInterrupted());
26 }
27 t1.join();
28 }
29
30 }
输出如下:
Connected to the target VM, address: '127.0.0.1:51128', transport: 'socket'
task start
task do sth
before notify, t1 state-0 :false
before notify, t1 state-1 :true
task catch interrupt ex ,state: false
task end
Disconnected from the target VM, address: '127.0.0.1:51128', transport: 'socket'
Process finished with exit code 0
大致逻辑是这样的:
主线程启动t1线程,t1线程抢到锁,t1处于wait状态(同时释放锁), 主线程抢到锁,主线程中断t1,t1需要再次抢到锁才能抛出中断异常,
当后续在抛出中断异常的时候,t1内部的中断标记也被重置了。
这样子即使线程处于join/wait/sleep等状态下,无法主动check状态,我们也可以通过抛出异常的形式,来中断线程了。同时根据前文中的jdk源码,也可以知道,即使在IO等待的状态下,也会被中断。而一开始说的api:interrupted()中的源码中,会调用clearInterruptEvent() 清理中断事件,其作用就是为了清理这些中断异常抛出等事件,保证虽然标记被恢复为false,对应的中断事件也都被清理干净了。
**另外还需要注意的是,中断是一个累加状态,也就是说即使当前线程还没开始阻塞(join/wait/sleep/io阻塞等),一旦被标记为中断,即使后边再遇到阻塞场景,也一样被中断。**来看这个例子:
1 import java.util.concurrent.TimeUnit;
2
3 /**
4 * @discription
5 */
6 public class InterruptEx1Study {
7 private static final Object lock = new Object();
8
9 static volatile boolean time_out = false;
10
11 public static void main(String[] args) throws InterruptedException {
12 Thread sleepTask = new Thread(() -> { //睡眠线程
13 try {
14 Thread.sleep(2000L);
15 } catch (InterruptedException e) {
16 // do sth
17 }
18 time_out = true;
19 });
20
21
22 Thread t1 = new Thread(() -> {
23 System.out.println("t1 start ,state:" + Thread.currentThread().isInterrupted());
24 sleepTask.start();
25
26 while (!time_out) {
27 //wait sleep task
28 }
29 System.out.println("t1 wait sleep task end,state:" + Thread.currentThread().isInterrupted());
30 synchronized (lock) {
31 System.out.println("t1 task start");
32 try {
33 System.out.println("t1 task do sth");
34 lock.wait();
35 } catch (InterruptedException e) {
36 System.out.println("t1 catch interrupt ex ,state: " + Thread.currentThread().isInterrupted());
37 }
38 System.out.println("task end ");
39 }
40 });
41
42 t1.start();
43 Thread.sleep(500L); //等待t1拉起睡眠线程
44 synchronized (lock) {
45 System.out.println("before notify, t1 state-0 :" + t1.isInterrupted());
46 t1.interrupt();
47 System.out.println("before notify, t1 state-1 :" + t1.isInterrupted());
48 }
49 t1.join();
50 }
51
52 }
大致逻辑是t1启动以后,先等待2s(通过等待另外一个线程修改完标记来做到),
2s结束后t1才开始获取锁,在获取锁之前主线程先拿到锁并且打上中断标记,并且释放锁。(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )
此时t1抢到锁,开始等待,此时由于之前已经被打上中断印记了,因此这里会直接抛出异常,结束任务。
实际输出如下:
Connected to the target VM, address: '127.0.0.1:52345', transport: 'socket'
t1 start ,state:false
before notify, t1 state-0 :false
before notify, t1 state-1 :true
t1 wait sleep task end,state:true
t1 task start
t1 task do sth
t1 catch interrupt ex ,state: false
task end
Disconnected from the target VM, address: '127.0.0.1:52345', transport: 'socket'
但是这里又会有一个新的问题,如果线程t1内部逻辑很复杂,方法套方法,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )出现了多个wait/sleep等阻塞动作怎么办,我们本意是希望终止线程,结果到处阻塞,我总不能在外边写一个while循环,不断的打断线程吧。
大可不必,一般推荐的办法是,在catch到终止异常时,判断是否真的要终止,还是需要继续执行剩余代码(一般后端守护线程都需要继续再执行)。
如果确定要终止,则在catch中直接调用Thread.currentThread().interrupt();直接自己给自己再打一个终止标记即可。
这样即使后边再遇到check 或者 阻塞的地方,都会直接按照已中断继续的来处理。
因此当有interrupt方法时,我们最好不要直接catch Exception 这样的大异常,使得维护阶段,忘记这里可能线程被中断,从而继续跑任务。