如何优雅地结束线程(二)

468 阅读3分钟

前言

上一篇我们介绍了两种结束线程的方式.其实它们都有一个缺点,那就是执行一个阻塞IO操作或计算非常耗时,当前线程根本无法判断中断标志或捕获中断请求.在这种情况下,我们必须通过其他方式来中断线程.也就是今天要说的第三种方式.在我看来它兼顾了IO密集型操作和计算密集型操作, 那它是如何实现的呢?

其实很简单,添加超时控制,如果在规定的时间没完成就强制结束.关键是涉及到一些技巧,具体过程如下:

  1. 在执行线程中另起一个守护线程来执行阻塞IO或密集计算,一旦执行线程结束,守护线程自然结束
  2. 添加专门的执行(execute)和中断(terminate)方法来执行和结束任务.中断方法中设置超时时间来应对任务长时间无法完成的情况
  3. 添加结束标志stopped来同步线程之间的状态

实现

先看一下execute方法:

  private static Thread worker;
  private static volatile boolean stopped=false;
  public static void execute(Runnable task){
    worker= new Thread(() -> {
      Thread daemon=new Thread(task);
      daemon.setDaemon(true);
      daemon.start();
      try {
        daemon.join();
        stopped=true;
      } catch (InterruptedException e) {
        //
      }
    });
    worker.start();
  }

方法接受一个耗时任务作为参数,执行线程不会直接执行任务,而是交给守护线程(daemon).注意,执行线程通过daemon.join()来等待守护线程执行完成,从而保证二者的状态一致.守护线程执行完将结束标志设置为true,从而通知其他线程.

再看一下terminate方法

  public static void terminate(long mills){
    long begin=System.currentTimeMillis();
    while (!stopped){
      if(System.currentTimeMillis()-begin>mills) {
        worker.interrupt();
        break;
      }
      try {
        Thread.sleep(1);
      } catch (InterruptedException e) {
        //log
      }
    }
    long end=System.currentTimeMillis();
    System.out.printf("terminated after %d ms\n",(end-begin));
  }

方法接受一个超时参数,当结束标志为false时,不断检测是否到达超时时间.如果到达,那么结束执行线程并退出,如果没有,则休息片刻后继续检测.

测试用例

下面是三个测试用例

用例1: 执行阻塞IO,三秒后强制结束

  private static void test1(){
    Runnable task=()->{
      try(InputStream stream=System.in){
        //waiting for user input
        System.out.println((char)stream.read());
      }
      catch (IOException ie){}
    };
    StopService.execute(task);
    StopService.terminate(3000L);
  }

注意: 执行阻塞IO操作时,即使其他线程调用interrupt方法,当前线程因为被阻塞,无法捕获InterruptedException异常

用例2: 执行计算耗时操作,三秒后强制结束

  private static void test2(){
    Runnable task=()->{
      while (true){}
    };
    StopService.execute(task);
    StopService.terminate(3000L);
  }

用例3: 如果碰巧某次操作很短比如一秒就结束了,那么不需要非的傻傻等到超时结束(3秒)

这就是为什么daemon.join()后将stopped=true

  private static void test3(){
    Runnable task=()->{
      ThreadUtil.sleep(1000L);
    };
    StopService.execute(task);
    StopService.terminate(3000L);
  }

我们学习了三种中断线程的方法:

  1. 添加中断标志
  2. 发送中断请求
  3. 兼容阻塞IO和计算耗时的方式 每种方式都有特定的使用场景,熟练掌握每种实现方式并了解它们的优劣对后期我们编写复杂的代码大有帮助.