1 线程阻塞
线程阻塞通常是指一个线程在执行过程中暂停,以等待某个条件的触发。而什么情况才会使得线程进入阻塞的状态呢?
等待阻塞:运行的线程执行 wait() 方法,该线程会释放占用的所有资源,JVM 会把该线程放入“等待池”中。进入这个等待状态 (WAITING) 后,是不能自动唤醒的,必须依靠其它线程调用 notify() 或 notifyAll() 方法才能被唤醒,或者是被中断后才能唤醒,然后被调度后开始运行或同步阻塞。
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁 (synchronized) 被别的线程占用,则 JVM 会把该线程放入“锁池”中,注意这种阻塞 (BLOCKED) 是不能被打断的。
还有运行的线程执行 sleep() 或 join() 方法,或者发出了可以中断的 I/O 请求时,JVM 会把该线程置为等待 (WAITING) 状态:当 sleep() 状态超时、join() 等待线程终止或者超时、或者可以中断的 I/O 处理完毕时,线程重新转入运行状态 (被调度后开始运行或等待调度);此外线程被中断后也能进入运行状态。
在 Java 中,最好的停止线程的方式是使用中断 interrupt,但是这仅仅是会通知到被终止的线程“你该停止运行了”,被终止的线程自身拥有决定权(决定是否、以及何时停止),这依赖于请求停止方和被停止方都遵守一种约定好的编码规范。任务和线程的启动很容易。在大多数时候,我们都会让它们运行直到结束。然而有时候我们希望提前结束任务或线程(或许是因为用户取消了操作或者服务需要被快速关闭,或者是运行超时或出错了)。
要使任务和线程能安全、快速、可靠地停止下来,并不是一件容易的事。Java 没有提供任何机制来安全地终止线程;但它提供了中断,这是一种协作机制,使用得当时能够使一个线程终止另—个线程的当前工作。
这种协作式的方法是必要的,我们很少希望某个任务、线程或服务立即停止,因为这种立即停止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务时可以使用种协作的方式,当需要停止时它们首先会清除当前正在执行的工作然后再结束。
这提供了更好的灵活性,因为任务本身的代码比发出取消请求的代码更清楚如何执行清除工作。生命周期结束的问题会使任务、服务以及程序的设计和实现等过程变得复杂,而这个在程序设计中是非常重要的要素却经常被忽略。一个在行为良好的软件与勉强运行的软件之间最主要区别就是,行为良好的软件能很完善地处理失败、关闭和取消等过程。
2 响应中断异常的常见方法
2.1 Object 各种 wait 方法
wait()
wait(long timeout, int nanos)
wait(long timeout)
2.2 lock.Condition 全部 await 方法
await()
awaitNanos(long nanosTimeout)
await(long time, TimeUnit unit)
awaitUntil(Date deadline)
2.3 Lock 部分加锁方法
只有 lockInterruptibly() 响应中断,下面其他加锁方法都不响应中断:
tryLock(long time, TimeUnit unit)
2.4 Thread 部分方法
sleep(long) 静态方法
sleep(long, int) 静态方法
join(long) 实例方法
join(long, int) 实例方法
join() 实例方法
3 中断相关方法
3.1 thread.interrupt()
该方法用于中断 Thread 线程,此线程并非当前线程,不是强制关闭线程,而是传递给目标线程一个“应该关闭”的信号,然后由目标线程自行处理。“应该关闭”的信号的具体体现:
当目标线程处于阻塞状态,会抛出 InterruptedException,而不会将中断标识位设置为 true。
当目标线程处于非阻塞状态,会将中断标识位设置为 true,不会抛出异常。
3.2 Thread.interrupted()
该方法是静态方法,调用时会重置当前线程的中断状态,如果线程被中断了,调用这个方法第一次会返回 true,调用第 n 次 (n>=2) 会返回 false;如果线程没有被中断,那么调用这个方法会返回 false,调用第 n 次 (n>=2) 会返回 false。
3.3 thread.isInterrupted()
该方法是实例方法,不会重置对应线程的中断状态,如果线程被中断了返回 true,否则返回 false。
public static class MyThred extends Thread {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("Someone interrupted me.");
} else {
System.out.println("Thread is Going...");
}
}
}
}
public static void main(String[] args) throws InterruptedException {
MyThred t = new MyThred();
t.start();
Thread.sleep(3000);
t.interrupt();
}
4 中断场景分析
如果我们有一个运行中的软件,例如是杀毒软件正在全盘查杀病毒,此时我们想让它停止杀毒,这时候点击取消,那么就是正在中断一个运行的线程。
每一个线程都有一个 boolean 类型的实例变量,此变量表示当前的线程是否收到了中断请求,默认为 false。当一个线程 A 调用了线程 B 的 interrupt 方法时,那么线程 B 的中断标识变为 true。而线程 B 可以调用某些方法检测到此标识的变化。
阻塞方法:如果线程 B 调用了能响应中断的阻塞方法,并且在阻断过程中或被阻断前中断标识变为了 true,那么它会抛出 InterruptedException 异常。抛出异常的同时它会将线程 B 的中断标识置为 false。
非阻塞方法:可以通过线程 B 的 isInterrupted 方法进行检测中断标识为 true 还是 false。另外还有一个静态的方法 interrupted 方法也可以检测标志,但是该静态方法检测完以后会自动将是否请求中断标识位置为 false。
下面为具体的例子:
4.1 等待时被中断
public class InterrupTest implements Runnable{
public void run(){
try {
while (true) {
Boolean flag = Thread.currentThread().isInterrupted();
System.out.println("isInterrupted=" + a);
Thread.sleep(20000);
System.out.println("sleep is over");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Boolean first=Thread.interrupted();
Boolean second=Thread.interrupted();
System.out.println("first="+first);
System.out.println("second="+second);
}
}
public static void main(String[] args) {
InterrupTest si = new InterrupTest();
Thread t = new Thread(si);
t.start();
try {
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
t.interrupt();
System.out.println("main has interrupted other thread");
System.out.println("main return");
}
}
打印的参数如下:
isInterrupted=false
main has interrupted other thread
main return
first=true
second=false
现在知道线程可以检测到自身的标志位的变化,但是它只是一个标志,如果线程本身不处理的话,那么程序还是会执行下去。
因此 interrupt 方法并不能立即中断线程,该方法仅仅告诉线程外部已经有中断请求,至于是否中断还取决于线程自己。
4.2 等待前已经被中断
public class InterrupTest implements Runnable {
public void run() {
try {
System.out.println("sleep for 20 seconds before, time="+ LocalTime.now());
Thread.sleep(20000);
System.out.println("sleep return, time="+ LocalTime.now());
} catch (InterruptedException e) {
System.out.println("it is interrupted while in sleep, time="+ LocalTime.now());
System.out.println("it is interrupted while in sleep,interrupted status = " + Thread.currentThread().isInterrupted());
Thread.currentThread().interrupt();
System.out.println("it is interrupted while in sleep,set to true , interrupted status = " + Thread.currentThread().isInterrupted());
}
System.out.println("run again, time="+ LocalTime.now());
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
System.out.println("it is interrupted again while in sleep, time="+ LocalTime.now());
}
System.out.println("run again,time="+ LocalTime.now());
}
public static void main(String[] args) {
InterrupTest si = new InterrupTest();
Thread t = new Thread(si);
t.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
System.out.println("main has interrupted other thread");
System.out.println("main return");
}
}
打印日志如下
sleep for 20 seconds before, time=09:01:38.022
main has interrupted other thread
main return
it is interrupted while in sleep,time=09:01:39.800
it is interrupted while in sleep,interrupted status = false
it is interrupted while in sleep,set to true , interrupted status = true
run again, time=09:01:39.800
it is interrupted again while in sleep, time=09:01:39.800
run again, time=09:01:39.800
4.3 线程开始运行前已经被中断
如果线程在开始运行前被中断不会把它的中断标识设置为 true,那么线程开始运行后是不会收到中断信号。
public class InterrupTest implements Runnable {
public void run() {
try {
System.out.println("sleep for 20 seconds before, time="+ LocalTime.now());
Thread.sleep(20000);
System.out.println("sleep return, time="+ LocalTime.now());
} catch (InterruptedException e) {
System.out.println("it is interrupted while in sleep, time="+ LocalTime.now());
System.out.println("it is interrupted while in sleep,interrupted status = " + Thread.currentThread().isInterrupted());
Thread.currentThread().interrupt();
System.out.println("it is interrupted while in sleep,set to true , interrupted status = " + Thread.currentThread().isInterrupted());
}
System.out.println("run again, time="+ LocalTime.now());
}
public static void main(String[] args) {
InterrupTest si = new InterrupTest();
Thread t = new Thread(si);
t.interrupt();
System.out.println("main has interrupted other thread before it start");
t.start();
System.out.println("main has started other thread");
System.out.println("main return");
}
}
打印日志如下
main has interrupted other thread before it start
main has started other thread
main return
sleep for 20 seconds before, time=09:06:05.552
run again, time=, time=09:06:25.556
5 InterruptedException 如何处理
通常线程会在什么情况下正常结束:运行完 run 方法就会正常停止线程,但是在运行任务过程中如何发生了中断,我们需要根据具体业务需求,来处理中断,可以分为以下场景。
5.1 中断后线程进入终止状态
通常我们会使用生产 - 消费者模式处理任务,而处理任务的逻辑是在一个 while 死循环中不断的获取然后处理任务;但是我们想把这个线程终止,那么可以对它执行 interrupt 方法,那么这个线程会在 take 等待时跳出 while 循环,并且在执行完 catch 语句后终止,线程一旦终止是不能重新运行的,这样我们也不用关心这个线程是否被中断过了。
public class TaskRunner implements Runnable {
private BlockingQueue<Task> queue;
public TaskRunner(BlockingQueue<Task> queue) {
this.queue = queue;
}
public void run() {
try {
while (true) {
Task task = queue.take(8, TimeUnit.SECONDS);
task.execute();
}
}
catch (InterruptedException e) {
Log.info("worker thread is terminated");
}
}
......
}
5.2 被中断后需要做业务处理
如果线程被中断后没有立刻停止,还需处理其它业务逻辑,那么我们需要把中断标识保留并传递给后续的业务处理逻辑。
下面是线程池运行任务的部分代码:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock();
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() && runStateAtLeast(ctl.get(), STOP)))
&& !wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
}
catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
5.3 在 while 外围使用 try/catch
如果在执行过程中,每次循环都会调用 sleep 或 wait 等方法,那么不需要每次迭代都检查是否已中断,sleep 或 wait 等方法会响应中断
public class Test {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
while (num <= 10000) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
Thread.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
打印结果:
0是100的倍数
100是100的倍数
200是100的倍数
300是100的倍数
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.hd.thread.stop.RightWayStopThreadWithSleepEveryLoop.lambda$main$0(RightWayStopThreadWithSleepEveryLoop.java:20)
at java.lang.Thread.run(Thread.java:745)
这个案例中我们不再需要每次检查是否中断,sleep() 阻塞时候会自动检测中断,即不再需要:Thread.currentThread().isInterrupted(),我们只需要用 try-catch 包围 while 循环即可,这样发生中断异常时也能跳出 while 循环。但是这样做有一个问题,如果我们在 sleep 结束后 num++ 执行前对线程发出中断,那么最终会只能 num++ 后在执行 sleep 时才能响应中断,如果业务不允许这种情况发生,那么我们需要调整代码在 num++ 操作执行前判断是否当前线程被中断了。
5.4 while 内 try/catch 的问题
如果 while 里面放 try/catch,会导致中断失效。所以我们需要在捕获中断异常后设置中断标识为 true,这样在后续处理时才能识别到中断请求
public class Test {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
while (num <= 10000 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
打印结果:
0是100的倍数
100是100的倍数
200是100的倍数
300是100的倍数
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.hd.thread.stop.CantInterrupt.lambda$main$0(CantInterrupt.java:20)
at java.lang.Thread.run(Thread.java:745)
400是100的倍数
500是100的倍数
600是100的倍数
...
Thread.sleep 检测到的中断信号抛出的异常被捕获,这样当前线程的中断标被重置为 false,所以 while 循环没有停止。
6 最佳实践
最佳实践:catch 了 InterruptedExcetion 之后的优先选择:在方法签名中抛出中断异常。对于下面代码,我们就可以在 run() 内使用 try/catch 捕获异常 (编译器会提示需要捕获中断异常)
public class Test implements Runnable {
@Override
public void run() {
while (true && !Thread.currentThread().isInterrupted()) {
System.out.println("start");
try {
throwInMethod();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("处理中断异常");
e.printStackTrace();
}
}
}
private void throwInMethod() throws InterruptedException {
Thread.sleep(2000);
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
打印结果:
start
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.xwy.Test.throwInMethod(Test.java:27)
at com.xwy.Test.run(Test.java:16)
at java.lang.Thread.run(Thread.java:745)
处理中断异常
处理中断的最好方法是什么?
优先选择在方法上抛出异常。用 throws Interrupted Exception 标记你的方法,而不是用 try 语句块捕获异常,这样以便于该异常可以传递到顶层,让 run 方法可以捕获这一异常,例如
private void throwInMethod() throws InterruptedException {
Thread.sleep(2000);
}
由于 run 方法内无法抛出 checked Exception(只能用 try catch),顶层方法必须处理该异常,避免了漏掉或者被吞掉的情况,增强了代码的健壮性。