文章目录
认识
对于 Java 而言,最正确的停止线程的方式是使用 interrupt。但 interrupt 仅仅起到通知被停止线程的作用。而对于被停止的线程而言,它拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。这个的意思就是说如果执行了interrupt,被中断的线程不会立刻停止执行任务,而是会根据自身任务执行情况选择停止,一般会等待任务执行完成后中断停止
为什么 Java 不提供强制停止线程的能力呢?
事实上,Java 希望程序间能够相互通知、相互协作地管理线程,因为如果不了解对方正在做的工作,贸然强制停止线程就可能会造成一些安全的问题,为了避免造成问题就需要给对方一定的时间来整理收尾工作。比如:线程正在写入一个文件,这时收到终止信号,它就需要根据自身业务判断,是选择立即停止,还是将整个文件写入成功后停止,而如果选择立即停止就可能造成数据不完整,不管是中断命令发起者,还是接收者都不希望数据出现问题。
interrupt介绍
public void interrupt() 中断这个线程。
我们一旦调用某个线程的 interrupt() 之后,这个线程的中断标记位就会被设置成 true。每个线程都有这样的标记位,当线程执行时,应该定期检查这个标记位,如果标记位被设置成 true,就说明有程序想终止该线程。
public class MyThread implements Runnable {
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted() && count < 10000) {
System.out.println("当前计数:"+count);
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyThread());
thread.start();
Thread.sleep(5);
//中断当前线程
thread.interrupt();
}
}
我们可以通过Thread.currentThread().isInterrupted()来检查当前线程是否被通知中断。上面代码中thread.interrupt();,在线程感应到中断信号后就不在继续执行了。这种就属于通过 interrupt 正确停止线程的情况。通过在程序中添加中断标记位的检查从而响应中断信号。
sleep、wait 期间能否感受到中断
public class MyThread1 implements Runnable {
@Override
public void run() {
int count = 0;
while(!Thread.currentThread().isInterrupted()&& count < 10000) {
System.out.println("当前计数:"+count);
count++;
try {
//设置线程休眠
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyThread1());
thread.start();
Thread.sleep(5);
//中断当前线程
thread.interrupt();
}
}
在响应线程中断信号的代码中设置线程休眠,修改代码如上所示,主线程休眠 5 毫秒后,通知子线程中断,此时子线程仍在执行 sleep 语句,处于休眠中。那么就需要考虑一点,在休眠中的线程是否能够感受到中断通知呢?是否需要等到休眠结束后才能中断线程呢?如果是这样,就会带来严重的问题,因为响应中断太不及时了。正因为如此,Java 设计者在设计之初就考虑到了这一点。
如果 sleep、wait 等可以让线程进入阻塞的方法使线程休眠了,而处于休眠中的线程被中断,那么线程是可以感受到中断信号的,并且会抛出一个 InterruptedException 异常,同时清除中断信号,将中断标记位设置成 false。这样一来就不用担心长时间休眠中线程感受不到中断了,因为即便线程还在休眠,仍然能够响应中断通知,并抛出异常。
上面代码执行情况如下,可以看到是可以响应中断信号的,此时会抛出一个 InterruptedException 异常,同时清除中断信号,将中断标记位设置成 false
俩种中断处理方式
(1)方法签名抛异常
1:对于响应线程中断的程序,直接通过catch处理异常时非常不友好的,我们可以在方法中使用 try/catch 或在方法签名中声明 throws InterruptedException。
void doTask() throws InterruptedException {
Thread.sleep(1000);
}
正如代码所示,要求每一个方法的调用方有义务去处理异常。调用方要不使用 try/catch 并在 catch 中正确处理异常,要不将异常声明到方法签名中。如果每层逻辑都遵守规范,便可以将中断信号层层传递到顶层,最终让 run() 方法可以捕获到异常。而对于 run() 方法而言,它本身没有抛出 checkedException 的能力,只能通过 try/catch 来处理异常。层层传递异常的逻辑保障了异常不会被遗漏,而对 run() 方法而言,就可以根据不同的业务逻辑来进行相应的处理。
(2)使用Thread.currentThread().interrupt();
在 catch 语句中再次中断线程。如代码所示,需要在 catch 语句块中调用 Thread.currentThread().interrupt() 函数。因为如果线程在休眠期间被中断,那么会自动清除中断信号。如果这时手动添加中断信号,中断信号依然可以被捕捉到。这样后续执行的方法依然可以检测到这里发生过中断,可以做出相应的处理,整个线程可以正常退出。
我们需要注意,我们在实际开发中不能盲目吞掉中断,如果不在方法签名中声明,也不在 catch 语句块中再次恢复中断,而是在 catch 中不作处理,我们称这种行为是“屏蔽了中断请求”。如果我们盲目地屏蔽了中断请求,会导致中断信号被完全忽略,最终导致线程无法正确停止。
volatile 应用线程中断的实践分析
(1)volatile 修饰标记位适用的场景
public class MyThread implements Runnable {
private volatile boolean flag = false;
@Override
public void run() {
int num = 0;
try {
while (!flag && num <= 1000000) {
num++;
Thread.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("线程终止");
}
}
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
Thread.sleep(3000);
myThread.flag = true;
}
}
如上面代码所示,在线程运行的run方法中通过一个volatile标识位来作为线程是否继续运行的判断标识。在主线程启动后休眠三秒后设置线程运行标识为true停止运行该线程,查看运行结果可以看到在这种情况下volatile是可以作为线程结束运行的标识位的
(2)volatile 修饰标记位不适用的场景
接下来我们就用一个生产者/消费者模式的案例来演示为什么说 volatile 标记位的停止方法是不完美的。
import java.util.concurrent.BlockingQueue;
public class Producer implements Runnable{
/** 标识位,默认为false **/
public volatile boolean flag=false;
/** 阻塞队列 **/
private final BlockingQueue blockingQueue;
public Producer(BlockingQueue blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
try {
while ( !flag ){
double random = Math.random();
blockingQueue.put(random);
System.out.println("生产数据"+random);
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("生产者线程结束");
}
}
}
上面构建了一个生产者线程,会根据flag 决定是否往阻塞队列中添加元素,如果线程运行结束则打印生产者线程结束
public class Consumer {
private final BlockingQueue blockingQueue;
public Consumer(BlockingQueue blockingQueue) {
this.blockingQueue = blockingQueue;
}
public boolean needMoreNums() {
return Math.random() <= 0.97;
}
}
消费者代码如上所示,这里没有直接在消费者端处理数据,而是在消费者提供了一个条件方法作为是否生产数据的逻辑条件
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(8);
Producer producer = new Producer(blockingQueue);
Thread thread = new Thread(producer);
thread.start();
//主线程休眠,保证生产者先生产足够的数据
Thread.sleep(500);
Consumer consumer = new Consumer(blockingQueue);
if (consumer.needMoreNums()){
System.out.println("消费数据:"+blockingQueue.take());
Thread.sleep(100);
}
producer.flag=true;
System.out.println(producer.flag);
}
测试方法如上,构建生产者线程并且让主线程休眠,保证生产者先生产足够的数据。其中在main方法中通过一个判断条件决定是否消费数据,如果不满足条件则不再消费数据并且通过标识位通知生产者可以退出生产数据。
如果我们运行上面的代码,我们会看到首先生产者是正常生产数据了,并且消费者也消费了一条数据,其次通知生产者中断的标识位也被赋值了,但是生产者线程并没有正常结束。
按照正常设计来说,标记位设置为 true,理论上此时生产者会跳出 while 循环,并打印输出“生产者运行结束”。
然而结果却不是我们想象的那样,尽管已经把 flag设置成 true,但生产者仍然没有停止,这是因为在这种情况下,生产者在执行 blockingQueue.put(num) 时发生阻塞,在它被叫醒之前是没有办法进入下一次循环判断 flag的值的,所以在这种情况下用 volatile 是没有办法让生产者停下来的,相反如果用 interrupt 语句来中断,即使生产者处于阻塞状态,仍然能够感受到中断信号,并做响应处理。
(3)知识扩展
这里需要简单扩展下阻塞队列的知识,使用put方法时如果阻塞队列已满,那么此时put方法会阻塞,等待消费者从阻塞队列中取出数据,上面的问题就在于设置了阻塞队列大小为8,这个阻塞队列在消费者取出后也再次填充满了,导致生产者线程处于阻塞状态,如果设置阻塞队列大小为一个很大的值,那么是可以正常结束
总结
【1】对于线程终止的方法比如比如 stop(),suspend() 和 resume(),这些方法已经被 Java 直接标记为 @Deprecated; 对于stop()来说 会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务戛然而止,会导致出现数据完整性等问题。而对于 suspend() 和 resume() 而言,它们的问题在于如果线程调用 suspend(),它并不会释放锁,就开始进入休眠,但此时有可能仍持有锁,这样就容易导致死锁问题,因为这把锁在线程被 resume() 之前,是不会被释放的。
【2】使用通知机制的方式结合interrupt()去中断线程的执行
【3】volatile 无法处理处于阻塞状态的线程,需要使用中断信号来处理
【4】用 interrupt 来请求中断,而不是强制停止,因为这样可以避免数据错乱,也可以让线程有时间结束收尾工作。使用Thread.currentThread().isInterrupted()结合thread.interrupt()来完成中断需求设计