Java中正确终止线程的方法
Thread类中有一个已经废弃的 stop() 方法,它可以终止线程,但由于它不管三七二十一,直接终止线程,所以被废弃了。比如,当线程被停止后还需要进行一些善后操作(如,关闭外部资源),使用这个方法就无能为力了。可以通过线程中断来实现线程终止。
首先来看一下Java线程中断的一些内容:
- Java平台为每个线程维护了一个布尔型的中断标记,可以通过下列方法获取该标记的值:
- interrupt() 中断某个线程
- isInterrupted() 返回该线程的中断标记
- interrupted() 返回并重置该线程的中断标记(置为false)
- 中断仅是发起线程对目标线程的一种请求,也就是说,目标线程对这种请求可以相应,也可以忽略。
- Java标准库中与线程阻塞相关的方法对中断的相应方式都是抛出 InterruptedException 异常,并且按照惯例,抛出异常前都会重置中断标记为false,因此这些方法会清空线程的中断标记。
- Java标准库中与线程阻塞相关的方法在进行阻塞前会判断中断标记是否为true,为true则抛出异常;如果在阻塞后调用中断方法的话,那么JVM会设置该线程的中断标记,然后将该线程唤醒,因此中断具有唤醒线程的作用。
由上面几点和第二句加粗的话可知,可以使用线程中断来实现线程终止,只要目标线程判断一下中断标记即可,即使被中断的线程正处于阻塞状态,也能把他唤醒起来终止;由第一句加粗的话可知,直接使用线程中断实现线程终止是存在风险的,因为可能调用了一些Java标准库的阻塞方法,而导致了中断标记被清空,也就无法获得中断标记了(总是false),因此需要自己创建一个中断标记配合使用。
如,下面是一个可中断的任务执行器,他会在每次执行任务前,判断一下自定i的终止标记和剩余的任务数(善后);提供的shutdown方法除了将工作线程中断外(主要作用是唤醒可能处于阻塞状态的任务),还会将终止交集 terminated 置为 true。
执行 main 方法,可以发现,首先会打印出“客户端调用了 shutdown 方法”,然后过了四秒,main线程才会终止,可知shutdown方法正确地将目标线程终止了。关于“按照惯例,Java标准库中抛出InterruptedException异常的和线程相关的阻塞方法会清空中断标记”,可以将条件中的 !interminated 替换成 !Thread.currentThread().isInterrupted(),然后再执行main方法测试,可以发现main线程始终无法终止,因为 sleep() 方法清空了中断标记,所以 !Thread.currentThread().isInterrupted() 始终为true,导致工作线程始终无法终止。
public class TerminableTaskRunner { // 存储要执行的任务 private final BlockingQueue<Runnable> tasks; // 线程终止标志 private volatile boolean terminated; // 剩余的任务数 private final AtomicInteger count; // 实际执行任务的线程 private volatile Thread workThread; public TerminableTaskRunner(int capacity) { this.tasks = new LinkedBlockingDeque<>(capacity); this.count = new AtomicInteger(0); this.workThread = new WorkThread(); workThread.start(); } public void submit(Runnable task) { this.tasks.add(task); this.count.incrementAndGet(); } public void shutdown() { terminated = true; // 线程终止标志,由于中断标志可能会被覆盖,所以需要自己创建一个标志 if (workThread != null) workThread.interrupt(); // 唤醒线程 } private class WorkThread extends Thread { @Override public void run() { Runnable task; try { while (!terminated || tasks.size() >= 1) { task = tasks.take(); try { task.run(); // 可能会清空当前线程的中断标记,如task.run()在内部调用的阻塞方法抛出了InterruptedException } catch (Throwable e) { e.printStackTrace(); } count.decrementAndGet(); } } catch (InterruptedException e) { // 一旦调用shutdown且tasks.take()阻塞住,就抛出该异常,没有任务要执行,直接终止 workThread = null; } } } public static void main(String[] args) { TerminableTaskRunner taskRunner = new TerminableTaskRunner(4); for (int i = 0; i < 4; i++) { taskRunner.submit(()->{ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { System.out.println("客户端调用了 shutdown 方法"); } }); } taskRunner.shutdown(); } }