停止线程
原理:使用interrupt来通知,而不是强制。
在用户主动取消,或需要服务快速关闭以及运行超时或出错时,需要主动停止线程。Java语言没有一种机制来安全正确的停止线程,但是提供了interrupt来通知另一个线程来停止当前的工作。
在Java中,最好的停止线程的方式是使用中断interrupt,但是这仅仅是会通知到被终止的线程“你该停止运行了”,被终止的线程自身拥有决定权(决定是否、以及何时停止),这依赖于请求停止方和被停止方都遵守一种约定好的编码规范。
这种协作式的方法是必要的,我们很少希望某个任务、线程或服务立即停止,因为这种立即停止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务时可以使用一种协作的方式:当需要停止时,它们首先会清除当前正在执行的工作,然后再结束。这提供了更好的灵活性,因为任务本身的代码比发出取消请求的代码更清楚如何执行清除工作。
所以Java设计者将停止的权利交给了被停止线程的本身。
线程停止的普通情况
- run方法大的所有代码运行完毕
- 有异常出现,且方法中没有异常捕获
正确的停止方法
- 通常情况下停止线程(主线程调用子线程的interrupt方法,且需要子线程配合,如果没有配合,就不会中断)
/**
* run方法内没有sleep或wait方法时,停止线程
* @author samy
* @date 2019/9/10 16:12
*/
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(5);
thread.interrupt();
}
}
- 线程被阻塞的情况下停止线程(阻塞线程被中断时,会响应这个中断,并抛出异常);很多锁工具类在底层也会使用类似的机制
/**
* 带sleep的中断线程的写法
* @author samy
* @date 2019/9/10 16:22
*/
public class RightWayStopThreadWithSleep {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
int num = 0;
while(!Thread.currentThread().isInterrupted() && num <= 300){
if (num % 100 == 0){
System.out.println(num + "是100的倍数");
}
num ++;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(500);
thread.interrupt();
}
}
- 线程每次迭代后都阻塞的情况下停止线程(与不是循环相比,不需要每次迭代中加入中断条件)
/**
* 如果在执行过程中,每次循环都会调用sleep或wait等方法
* @author samy
* @date 2019/9/10 16:36
*/
public class RightWayStopThreadWithLoop {
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();
}
}
如果我们在循环内部加入try-catch会发生什么呢,我们来看下运行情况。代码如下:
public class CantInterrupt {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
while (num <= 10000){
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();
}
}
结果发现,并没有在子线程发生中断,一直运行了下去
这种情况发生的原因是子线程确实在sleep时响应了中断,但是抛出的InterruptedException是被检查异常,已经被try/catch处理了,所以还是会进行下一次循环。
如果我们在循环条件中加入中断条件会发生什么呢?
结果还是相同,子线程并没有停止下来,这个情况发生的原因是Java线程在响应中断后,会把interrupt标记位设置为false。
那么我们到底应该如何处理这样的问题呢?
实际开发中的两种最佳实践
- 在方法中抛出异常,由顶层方法处理
用throws InterruptedException标记方法,不采用try语句捕获异常,以便于该异常可以传递到顶层,让run方法可以捕获。
由于run方法内无法抛出checked Exception,顶层方法必须处理该异常,避免了漏掉或者被吞掉的情况,增加了代码的健壮性。
public class RightWayStopThreadInProd implements Runnable{
@Override
public void run() {
while (true){
try {
throwInMethod();
} catch (InterruptedException e) {
//添加日志,停止程序
System.out.println("报警");
e.printStackTrace();
}
}
}
private void throwInMethod() throws InterruptedException {
Thread.sleep(1000);
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd());
thread.start();
Thread.sleep(500);
thread.interrupt();
}
}
- 无法传递或不想:恢复中断
如果不想或无法传递InterruptedException(例如用run方法的时候,就不让该方法throws InterruptedException),那么应该选择在catch 子句中调用Thread.currentThread().interrupt() 来恢复设置中断状态,以便于在后续的执行依然能够检查到刚才发生了中断。
public class RightWayStopThreadInProd2 implements Runnable{
@Override
public void run() {
while (true){
if (Thread.currentThread().isInterrupted()){
System.out.println("程序运行结束");
break;
}
reInterrupt();
}
}
private void reInterrupt() {
try {
Thread.sleep(1000);
} 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(500);
thread.interrupt();
}
}
错误的停止方法
- 被弃用的stop,suspend和resume方法
虽然stop会释放所有的monitor,但是它本质上是不安全的,会造成数据错乱。
suspend并不会破坏对象,但是会把带锁的线程挂起,从而很容易造成死锁,如需要唤醒挂起线程的线程也加了相同锁的情况。
- 用volatile设置boolean标记位
当线程阻塞时,volatile是无法停止线程的
如生产者消费者问题中,如果生产者的生产速度大于消费者消费的速度,生产者因为阻塞队列已满而无法将产品放入,一直阻塞在put方法,这时就算消费者将boolean标记为false也无法停止线程。具体代码如下:
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();
Customer customer = new Customer(storage);
while(customer.needMoreNums()){
System.out.println(storage.take() + "被消费了");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了");
producer.canceled = true;
}
}
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 <= 10000 && !canceled){
if (num % 10 == 0){
storage.put(num);
System.out.println(num + "放入了仓库中");
}
num++;
Thread.sleep(1);
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
class Customer{
BlockingQueue storage;
public Customer(BlockingQueue storage) {
this.storage = storage;
}
public boolean needMoreNums() {
if (Math.random() > 0.9){
return false;
}
return true;
}
}
停止线程相关重要函数
- static boolean interrupted()
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
返回执行方法线程的中断标记,并清除标记位
- Thread boolean isInterrupted()
public boolean isInterrupted() {
return isInterrupted(false);
}
返回线程的中断标记
- Thread interrupt()
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
设置线程的中断标记