停止线程的正确方法
原理:通过interrup通知线程,而不是强制中断
Java中停止线程的原则是什么?
在Java中,最好的停止线程的方式是使用中断interrupt,但是这仅仅是会通知到被终止的线程“你该停止运行了”,被终止的线程自身拥有决定权(决定是否、以及何时停止),这依赖于请求停止方和被停止方都遵守一种约定好的编码规范。
任务和线程的启动很容易。在大多数时候,我们都会让它们运行直到结束,或者让它们自行停止。然而,有时候我们希望提前结束任务或线程,或许是因为用户取消了操作,或者服务需要被快速关闭,或者是运行超时或出错了。要使任务和线程能安全、快速、可靠地停止下来,并不是一件容易的事。Java没有提供任何机制来安全地终止线程。但它提供了中断( Interruption),这是一种协作机制,能够使一个线程终止另一个线程的当前工作。
这种协作式的方法是必要的,我们很少希望某个任务、线程或服务立即停止,因为这种立即停止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务时可以使用一种协作的方式:当需要停止时,它们首先会清除当前正在执行的工作,然后再结束。这提供了更好的灵活性,因为任务本身的代码比发出取消请求的代码更清楚如何执行清除工作。生命周期结束(End-of-Lifecycle)的问题会使任务、服务以及程序的设计和实现等过程变得复杂,而这个在程序设计中非常重要的要素却经常被忽略。一个在行为良好的软件与勉强运的软件之间的最主要区别就是,行为良好的软件能很完善地处理失败、关闭和取消等过程。
通常线程会在什么情况下停止?
- run方法执行完毕
- 抛出异常未捕获
正确停止线程:interrupet
通常线程会在什么情况下停止—普通情况
/**
* 描述: run方法内没有sleep或wait方法时,停止线程
*/
public class RightWayStopThreadWithoutSleep implements Runnable {
@Override
public void run() {
int num = 0;
//本线程收到中断信号,决定是否中断
while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE / 2) {
if (num % 10000 == 0) {
System.out.println(num + "是10000的倍数");
}
num++;
}
System.out.println("任务运行结束了");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
thread.start();
Thread.sleep(2000);
thread.interrupt();//在主线程通知线程中断
}
}
…
11073690000是10000的倍数
1073700000是10000的倍数
1073710000是10000的倍数
1073720000是10000的倍数
1073730000是10000的倍数
1073740000是10000的倍数
任务运行结束了
两秒后收到中断下行信号停止循环
线程可能被阻塞
/**
* 描述: 带有sleep的中断线程的写法
*/
public class RightWayStopThreadWithSleep {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
while (num <= 300000000 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
}
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(500);
thread.interrupt();/线程睡眠中断会抛出异常
}
}
9884200是100的倍数
9884300是100的倍数
9884400是100的倍数
9884500是100的倍数
9884600是100的倍数
9884700是100的倍数
9884800是100的倍数
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.daxin.netty_demo.RightWayStopThreadWithSleep.lambda$0(RightWayStopThreadWithSleep.java:18)
at java.lang.Thread.run(Thread.java:748)
如果每次循环都都中断
/**
* 描述: 如果在执行过程中,每次循环都会调用sleep或wait等方法,那么不需要每次迭代都检查是否已中断
*/
public class RightWayStopThreadWithSleepEveryLoop {
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();//五秒钟后抛出异常
}
}
如果while里面放try/catch,会导致中断失效
/**
* 描述: 如果while里面放try/catch,会导致中断失效
*/
public class CantInterrupt {
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的倍数
400是100的倍数
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.daxin.demo.CantInterrupt.lambda$0(CantInterrupt.java:16)
at java.lang.Thread.run(Thread.java:748)
500是100的倍数
600是100的倍数
700是100的倍数
800是100的倍数
900是100的倍数
1000是100的倍数
1100是100的倍数
1200是100的倍数
1300是100的倍数
1400是100的倍数
…
因为try/catch了异常所以循环会继续执行下去,在循环判断是否中断失效,因为sleep会清除中断标记
最佳实践
优先选择:传递中断:
优先选择在方法上抛出异常。
用throws InterruptedException 标记你的方法,不采用try 语句块捕获异常,以便于该异常可以传递
到顶层,让run方法可以捕获这一异常,例如:
void subTask() throws InterruptedException
sleep(delay);
}
由于run方法内无法抛出checked Exception(只能用try catch),顶层方法必须处理该异常,避免了
漏掉或者被吞掉的情况,增强了代码的健壮性。
**
/* * 描述: 最佳实践:catch了InterruptedExcetion之后的优先选择:在方法签名中抛出异常 那么在run()就会强制try/catch
*/
public class RightWayStopThreadInProd implements Runnable {
@Override
public void run() {
while (true && !Thread.currentThread().isInterrupted()) {
System.out.println("go");
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();
}
}
go
保存日志
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.daxin.demo.RightWayStopThreadInProd.throwInMethod(RightWayStopThreadInProd.java:24)
at com.daxin.demo.RightWayStopThreadInProd.run(RightWayStopThreadInProd.java:13)
at java.lang.Thread.run(Thread.java:748)
恢复中断
如果不想或无法传递InterruptedException(例如用run方法的时候,就不让该方法throws
InterruptedException),那么应该选择在catch 子句中调用Thread.currentThread().interrupt() 来恢复设置中断状态,以便于在后续的执行依然能够检查到刚才发生了中断。在这里,线程在sleep期间被中断,并且由catch捕获到该中断,并重新设置了中断状态,以便于可以在下一个循环的时候检测到中断状态,正常退出。
/**
1. 描述:最佳实践2:在catch子语句中调用Thread.currentThread().interrupt()来恢复设置中断状态,以便于在后续的执行中,依然能够检查到刚才发生了中断
2. 回到刚才RightWayStopThreadInProd补上中断,让它跳出
*/
public class RightWayStopThreadInProd2 implements Runnable {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("Interrupted,程序运行结束");
break;
}
reInterrupt();
}
}
private void reInterrupt() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();//重新设置中断标记
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd2());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
如果子方法不重新设置中断标记那么程序会一直while下去
响应中断常规情况:
错误停止方法:
- 被弃用的stop、suspend和ruseme
stop线程不安全,suspend不释放锁休眠容易死锁
/**
* 描述: 错误的停止方法:用stop()来停止线程,会导致线程运行一半突然停止,没办法完成一个基本单位的操作(一个连队),会造成脏数据(有的连队多领取少领取装备)。
*/
public class StopThread implements Runnable {
@Override
public void run() {
//模拟指挥军队:一共有5个连队,每个连队10人,以连队为单位,发放武器弹药,叫到号的士兵前去领取
for (int i = 0; i < 5; i++) {
System.out.println("连队" + i + "开始领取武器");
for (int j = 0; j < 9; j++) {
System.out.println(j);
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("连队"+i+"已经领取完毕");
}
}
public static void main(String[] args) {
Thread thread = new Thread(new StopThread());
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.stop();
// Thread.interrupted();
}
}
4
5
6
7
8
连队2已经领取完毕
连队3开始领取武器
0
1
2
3
4
5
从执行结果看3号联队没有领取完就结束了,stop不能保证一个基本单位操作
如果用interrupted()中断线程结果如下:
0
1
2
3
4
5
6
7
8
连队4已经领取完毕
2、用volatile设置boolean标志位
/**
* 描述: 演示用volatile的局限:part1 看似可行
*/
public class WrongWayVolatile implements Runnable {
private volatile boolean canceled = false;
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !canceled) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数。");
}
num++;
Thread.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
WrongWayVolatile r = new WrongWayVolatile();
Thread thread = new Thread(r);
thread.start();
Thread.sleep(5000);
r.canceled = true;
}
}
…
1500是100的倍数。
1600是100的倍数。
1700是100的倍数。
1800是100的倍数。
1900是100的倍数。
2000是100的倍数。
2100是100的倍数。
2200是100的倍数。
2300是100的倍数。
2400是100的倍数。
2500是100的倍数。
2600是100的倍数。
2700是100的倍数。
2800是100的倍数。
正常停止
/**
* 描述: 演示用volatile的局限part2 陷入阻塞时,volatile是无法线程的 此例中,生产者的生产速度很快,消费者消费速度慢,所以阻塞队列满了以后,生产者会阻塞,等待消费者进一步消费
*/
public class WrongWayVolatileCantStop {
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
Producer producer = new Producer(storage);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(1000);
Consumer consumer = new Consumer(storage);
while (consumer.needMoreNums()) {
System.out.println(consumer.storage.take()+"被消费了");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了。");
//一旦消费不需要更多数据了,我们应该让生产者也停下来,但是实际情况
producer.canceled=true;
System.out.println(producer.canceled);
}
}
class Producer implements Runnable {
public volatile boolean canceled = false;
BlockingQueue storage;
public Producer(BlockingQueue storage) {
this.storage = storage;
}
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !canceled) {
if (num % 100 == 0) {
storage.put(num);
System.out.println(num + "是100的倍数,被放到仓库中了。");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者结束运行");
}
}
}
class Consumer {
BlockingQueue storage;
public Consumer(BlockingQueue storage) {
this.storage = storage;
}
public boolean needMoreNums() {
if (Math.random() > 0.95) {
return false;
}
return true;
}
}
程序会无限阻塞在storage.put(num);所以程序会无限执行下去
/**
* 描述: 用中断来修复刚才的无尽等待问题
*/
public class WrongWayVolatileFixed {
public static void main(String[] args) throws InterruptedException {
WrongWayVolatileFixed body = new WrongWayVolatileFixed();
ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
Producer producer = body.new Producer(storage);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(1000);
Consumer consumer = body.new Consumer(storage);
while (consumer.needMoreNums()) {
System.out.println(consumer.storage.take() + "被消费了");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了。");
producerThread.interrupt();
}
class Producer implements Runnable {
BlockingQueue storage;
public Producer(BlockingQueue storage) {
this.storage = storage;
}
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
storage.put(num);
System.out.println(num + "是100的倍数,被放到仓库中了。");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者结束运行");
}
}
}
class Consumer {
BlockingQueue storage;
public Consumer(BlockingQueue storage) {
this.storage = storage;
}
public boolean needMoreNums() {
if (Math.random() > 0.95) {
return false;
}
return true;
}
}
}
…
4600是100的倍数,被放到仓库中了。
3700被消费了
4700是100的倍数,被放到仓库中了。
3800被消费了
4800是100的倍数,被放到仓库中了。
3900被消费了
4900是100的倍数,被放到仓库中了。
4000被消费了
5000是100的倍数,被放到仓库中了。
消费者不需要更多数据了。
java.lang.InterruptedException
生产者结束运行
at java.util.concurrent.locks.AbstractQueuedSynchronizer
C
o
n
d
i
t
i
o
n
O
b
j
e
c
t
.
r
e
p
o
r
t
I
n
t
e
r
r
u
p
t
A
f
t
e
r
W
a
i
t
(
A
b
s
t
r
a
c
t
Q
u
e
u
e
d
S
y
n
c
h
r
o
n
i
z
e
r
.
j
a
v
a
:
2014
)
a
t
j
a
v
a
.
u
t
i
l
.
c
o
n
c
u
r
r
e
n
t
.
l
o
c
k
s
.
A
b
s
t
r
a
c
t
Q
u
e
u
e
d
S
y
n
c
h
r
o
n
i
z
e
r
ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2014) at java.util.concurrent.locks.AbstractQueuedSynchronizer
ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2014)atjava.util.concurrent.locks.AbstractQueuedSynchronizerConditionObject.await(AbstractQueuedSynchronizer.java:2048)
at java.util.concurrent.ArrayBlockingQueue.put(ArrayBlockingQueue.java:353)
at com.daxin.demo.WrongWayVolatileFixed$Producer.run(WrongWayVolatileFixed.java:47)
at java.lang.Thread.run(Thread.java:748)
用interrupet中断线程执行正常
/**
* 描述: 注意Thread.interrupted()方法的目标对象是“当前线程”,而不管本方法来自于哪个对象
*/
public class RightWayInterrupted {
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
for (; ; ) {
}
}
});
// 启动线程
threadOne.start();
//设置中断标志
threadOne.interrupt();
//获取中断标志
System.out.println("isInterrupted: " + threadOne.isInterrupted());
//获取中断标志并重置
System.out.println("isInterrupted: " + threadOne.interrupted());
//获取中断标志并重直
System.out.println("isInterrupted: " + Thread.interrupted());
//获取中断标志
System.out.println("isInterrupted: " + threadOne.isInterrupted());
threadOne.join();
System.out.println("Main thread is over.");
}
}
isInterrupted: true
isInterrupted: false
isInterrupted: false
isInterrupted: true
源码分析此方法会清除中断,无论谁调用只判断执行线程的中断状态
此方法不会清除中断状态
上篇:如何启动线程
如何处理不可中断的阻塞:
例如抢占锁时ReentratLock.lock()或者socket
I/O时无法响应中断)如果线程由于调用了wait()或join()方法,你可以中断线程,通过抛出InterrupetedExceprtion异常来唤醒该线程。但是对于不能响应IterruptedException的阻塞,很遗憾并没有用一个通用的结局方案。但是我们可以利用特定的其他响应中断的方法,比如ReentratLock.lockinterruptibly(),比如套接字是线程返回等方法来来达到目的。答案有很多种,因为有很多原因会造成线程阻塞,所以针对不同的情况,换起的方法也不同。
总结就是如果不支持响应中断,就用特定的方法唤起,没有万能药