Java如何优雅地停止一个线程?

  • 如何粗暴的停止一个线程?
  • 如何优雅的停止?
  • 打招呼的人(设置中断信号)
  • 怎么监听中断信号?
  • 如果线程休眠了我怎么打断?
  • 线程池中线程的打断方式


如何粗暴的停止一个线程?

没有粗暴做对比,就体现不出何为优雅。先说一个大家常用的方式:

kill -9 <pid> //没看错,和进程一样

而与之类似的,在java里有一种粗暴停止线程的方法(当前已经被废弃了,不推荐使用):

thread.stop(); //thread即要停止的线程对象

这种做法会粗暴的停止线程,不管当前线程是否在处理任务,这样极有可能会导致一些未知错误。比如:你正在写一个文件,然后写到一半线程被杀掉了,这个文件就损坏了。

如何优雅的停止?

我们会抱怨说:你想停没有问题,能不能让我把事情处理完你再停,或者你告诉我一声,我打个日志也行啊。这就是粗暴停止的坏处,他压根不通知你。

所以我们自然会想到,如果在停止时候能告诉我一声,我自己决定怎么停止,那该多好。

你既然敢想,java就敢做!于是乎新的API来了!!!

打招呼的人(设置中断信号)

为了满足优雅的停止的要求,java设计了一个方法:

thread.interrupt(); //thread即要停止的线程对象

通过调用此方法,在线程内部会收到一个信号(我们可以理解是打个招呼告诉你有其他人要你停下来),这样我们就可以开始执行停止工作了。

怎么监听中断信号?

监听这个信号很简单,Java提供了两个监听信号的方法:

  • 静态方法
boolean interrupted = Thread.interrupted();
  • 对象方法
boolean interrupted = Thread.currentThread().isInterrupted();

这两个方法都可以监听到线程是否被打断,但是有一个很大的区别:

  • 静态方法在调用后会清除停止信号,即如果你调用第二次,返回的将是false
  • 对像方法只是检测是否有停止信息,并不会清除状态,可以被反复调用。

所以,当你想清除停止信号(不想去理会,或者已经处理完了,不想后面再继续处理),可以使用静态方法,否则可以用对象方法。

说了那么多,我们来看个例子:

public class Worker implements Runnable {
    @Override
    public void run() {
        while (true) {
            //检测是否有终端信号
            if (Thread.interrupted()) {
                System.out.println("被打断,break");
                break;
            }
        }
        if (Thread.interrupted()) { 
        	//这里不会被执行
            System.out.println("被打断,二次检测");
        }
    }
}

Thread thread = new Thread(new Worker());
thread.start();
thread.interrupt(); //打断

输出:被打断,break

如果线程休眠了我怎么打断?

从上面的例子可以看到,如果线程顺利的运行到了信号检测的位置,我们就可以检测到信号,然后结束线程。但是如果线程处在休眠状态呢?比如:Thread.sleep(10000)

这时候就不得不说thread.interrupt()第二个功能了,当线程处在休眠状态时,调用thread.interrupt()会让线程从Thread.sleep(xx)处抛出InterruptedException异常,从而打断休眠态,让我们有机会处理这个信号。

举个例子:

public static class Worker implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(10000);
        } catch (Exception e) {
            if (e instanceof InterruptedException) {
                System.out.println("被打断");
            }
        }
        if (Thread.interrupted()) { 
        	//这里不会被执行
            System.out.println("被打断2");
        }
    }
}

Thread thread = new Thread(new Worker());
thread.start();
thread.interrupt(); //打断

输出:被打断

这里并不会输出“被打断2”,因为在抛出异常时,打断信息已经被清除了。

注意:这里还要提一下:

  • 以下方法也支持抛出InterruptedException异常打断:
    wait(), wait(long), wait(long, int), join(), join(long), join(long, int), sleep(long), sleep(long, int)
  • 对于实现了InterruptibleChannel接口的新版I/O类也支持打断,不过抛出的异常是:java.nio.channels.ClosedByInterruptException
  • 对于被Selector阻塞的场景,selector会立马返回,并且设置线程的打断状态。

特殊的:对于老的I/O类,比如:Socket,InputStream等是不支持打断的,所以要中断的话,需要从外部close掉,这样会引发内部抛出IOException,需要处理!(这里有一个点,socket在connecting的时候调用close方法无效,暂时没找到什么好办法停止正在连接的socket,有知道的朋友可以评论区说一下,感谢!)

线程池中线程的打断方式

直接看例子:

ExecutorService executorService = Executors.newFixedThreadPool(1);
Future<?> submit = executorService.submit(new Worker());
submit.cancel(true); //此处cancel的参数是:mayInterruptIfRunning
  • 如果任务已完成、已被取消或由于某些其他原因无法取消,则此尝试将失败。
  • 如果成功,并且在调用取消时此任务尚未启动,则此任务不应该运行。
  • 如果任务已经开始,则 mayInterruptIfRunning 参数确定是否应该中断执行该任务的线程以尝试停止该任务(调用interrupt()方法)。