使用后台进程和 Shutdown Hook 友好地关闭 Tomcat

最近的几篇博客里我讨论了长轮询(long polling)和Spring的DeferredResult技术,而且利用这些概念将生产者消费者项目塞进了一个Web应用程序。 尽管博客中的示例代码展现了相关概念,却也包含了不少逻辑问题。除了在实际的应用程序中不会使用简单的LinkedBlockingQueue而是选择JMS或者其余强健的消息队列服务,而且只会有一个用户能够得到匹配更新。还有一个严重的问题就是在JVM关闭时,行为不良的线程不会被关闭。java


你可能会问:为何这会成为问题……好吧,对程序员来讲这真的算不上一个问题,只要随便写点代码就能够解决。可是对使用软件的人而言这却会带来没必要要的麻烦。缘由是这样会产生不少行为不良的线程,而执行Tomcat的shutdown.sh命令收效甚微。这时你不得不执行下面命令野蛮的杀掉web服务器:git


ps -ef | grep java程序员


先获得进程pid,而后github


kill -9 <<pid>>web


……接着须要有一大片web服务器须要重启,这种混乱绝对让人头痛。最后你执行shutdown.sh中止Tomcat。apache


在我最近的几篇博客里,我编写的那些行为不良的线程在run()方法开头都包含了下面的代码:安全


@Override服务器

public void run() { 并发

 

  while (true) { app

    try { 

 

      DeferredResult<Message> result = resultQueue.take(); 

      Message message = queue.take(); 

 

      result.setResult(message); 

 

    } catch (InterruptedException e) { 

      throw new UpdateException("Cannot get latest update. " + e.getMessage(), e); 

    } 

  } 

}


在上面的代码里,我用了一个无限循环while(true),这意味着线程会一直运行而且不会终止。


@Override

public void run() { 

 

  sleep(5); // Sleep等待app从新加载 

 

  logger.info("The match has now started..."); 

  long now = System.currentTimeMillis(); 

  List<Message> matchUpdates = match.getUpdates(); 

 

  for (Message message : matchUpdates) { 

 

    delayUntilNextUpdate(now, message.getTime()); 

    logger.info("Add message to queue: {}", message.getMessageText()); 

    queue.add(message); 

  } 

  start = true; // 结束,重启 

  logger.warn("GAME OVER"); 

}


上面第二个示例中线程的行为一样很糟糕。线程会从MatchUpdates列表中取消息并在合适的时候添加到消息队列。惟一的可取之处是他们会抛出异常InterruptedException,若是正确处理线程能够正常中止。然而,没有人能确保这一点。


对上面代码的一个有效地快速修正……只要确保建立全部线程都是后台线程。后台线程的定义是:在程序结束时,即便线程还在运行但不会阻止JVM退出。一个后台线程的例子就是JVM的垃圾回收线程。将线程设置为后台线程只须要调用:


thread.setDaemon(true);


……接着执行shutdown.sh,而后砰的一声全部的线程都消失了。然而这种作法有一个问题:若是你的后台线程正在执行重要的任务,刚刚开始执行就被忽然结束掉会致使丢失不少重要的数据该怎么办?


你须要确保全部线程都被友好地关闭,在关闭前完成全部正在执行的任务。本文接下来将为这些错误的线程给出一个修复,使用ShutdownHook让他们在关闭前互相协调。根据文档的描述:“一个shutdown hook就是一个初始化但没有启动的线程。 当虚拟机开始执行关闭程序时,它会启动全部已注册的shutdown hook(不按前后顺序)而且并发执行。”读完最后一句话,你可能已经猜到了你须要的就是建立一个负责关闭多有其余线程的线程并经过shutdown hook传递给JVM。只要在你已有线程的run() 方法里用几个小的class作一些手脚。


须要建立ShutdownService和Hook两个类。首先展现的是Hook类,它会将ShutdownService 链接到你的线程,代码以下:


public class Hook { 

 

  private static final Logger logger = LoggerFactory.getLogger(Hook.class); 

 

  private boolean keepRunning = true; 

 

  private final Thread thread; 

 

  Hook(Thread thread) { 

    this.thread = thread; 

  } 

 

  /** 

   * @return True 若是后台线程继续运行

   */ 

  public boolean keepRunning() { 

    return keepRunning; 

  } 

 

  /** 

   * 告诉客户端后台线程关闭并等待友好地关闭

   */ 

  public void shutdown() { 

    keepRunning = false; 

    thread.interrupt(); 

    try { 

      thread.join(); 

    } catch (InterruptedException e) { 

      logger.error("Error shutting down thread with hook", e); 

    } 

  } 

}


Hook包含两个实例变量:keepRunning和thread。thread是对Hook负责关闭实例对象的引用,而keepRunning则是告诉线程继续运行。


Hook有两个public方法:keepRunning()和shutdown()。线程调用keepRunning()来确认是否须要继续运行,而shutdown()是由ShutdownService的shutdown hook线程调用以关闭目标线程。这就是两个方法的有趣之处。首先将keepRunning变量置为false,接着调用thread.interrupt()来打断线程强制抛出一个InterruptedException,最后调用thread.join()等待线程实例关闭。


值得注意的是这种方法须要你的线程配合。若是其中某个线程出错,那么全部的工做都会失败。为了不这种状况最好在thread.join(…)中加入一个超时。


@Service

public class ShutdownService { 

 

  private static final Logger logger = LoggerFactory.getLogger(ShutdownService.class); 

 

  private final List<Hook> hooks; 

 

  public ShutdownService() { 

    logger.debug("Creating shutdown service"); 

    hooks = new ArrayList<Hook>(); 

    createShutdownHook(); 

  } 

 

  /** 

   * Protected for testing 

   */

  @VisibleForTesting

  protected void createShutdownHook() { 

    ShutdownDaemonHook shutdownHook = new ShutdownDaemonHook(); 

    Runtime.getRuntime().addShutdownHook(shutdownHook); 

  } 

 

  protected class ShutdownDaemonHook extends Thread { 

 

    /** 

     * 循环并使用hook关闭全部后台线程

     * 

     * @see java.lang.Thread#run() 

     */

    @Override

    public void run() { 

 

      logger.info("Running shutdown sync"); 

 

      for (Hook hook : hooks) { 

        hook.shutdown(); 

      } 

    } 

  } 

 

  /** 

   * 建立hook class的新实例 

   */

  public Hook createHook(Thread thread) { 

 

    thread.setDaemon(true); 

    Hook retVal = new Hook(thread); 

    hooks.add(retVal); 

    return retVal; 

  } 

 

  @VisibleForTesting

  List<Hook> getHooks() { 

    return hooks; 

  } 

}


ShutdownService是一个Spring服务包含一个由引用的线程提供的Hook类列表用来关闭线程。它还包括了一个继承了Thread的内部类ShutdownDaemonHook。在ShutdownService构造函数中会建立一个ShutdownDaemonHook实例并传递给JVM做为shutdown hook,代码以下:


Runtime.getRuntime().addShutdownHook(shutdownHook);


ShutdownService 有一个public方法:createHook()。createHook()作的第一步是确保全部传递的线程都被设置为后台线程。接下来会建立一个新的Hook实例,在最终存储结果到列表返回给调用者以前做为参数传递给线程。


最后要作的就是将ShutdownService继承到DeferredResultService和MatchReporter。这两个类包含了行为不良的线程。


@Service("DeferredService") 

public class DeferredResultService implements Runnable { 

 

  private static final Logger logger = LoggerFactory.getLogger(DeferredResultService.class); 

 

  private final BlockingQueue<DeferredResult<Message>> resultQueue = new LinkedBlockingQueue<>(); 

 

  private Thread thread; 

 

  private volatile boolean start = true; 

 

  @Autowired

  private ShutdownService shutdownService; 

 

  private Hook hook; 

 

  @Autowired

  @Qualifier("theQueue") 

  private LinkedBlockingQueue<Message> queue; 

 

  @Autowired

  @Qualifier("BillSkyes") 

  private MatchReporter matchReporter; 

 

  public void subscribe() { 

    logger.info("Starting server"); 

    matchReporter.start(); 

    startThread(); 

  } 

 

  private void startThread() { 

 

    if (start) { 

      synchronized (this) { 

        if (start) { 

          start = false; 

          thread = new Thread(this, "Studio Teletype"); 

          hook = shutdownService.createHook(thread); 

          thread.start(); 

        } 

      } 

    } 

  } 

 

  @Override

  public void run() { 

 

    logger.info("DeferredResultService - Thread running"); 

    while (hook.keepRunning()) { 

      try { 

 

        DeferredResult<Message> result = resultQueue.take(); 

        Message message = queue.take(); 

 

        result.setResult(message); 

 

      } catch (InterruptedException e) { 

        System.out.println("Interrupted when waiting for latest update. " + e.getMessage()); 

      } 

    } 

    System.out.println("DeferredResultService - Thread ending"); 

  } 

 

  public void getUpdate(DeferredResult<Message> result) { 

    resultQueue.add(result); 

  } 

 

}


为DeferredResultService作的第一个修改就是自动匹配ShutdownService实例。接着在线程建立之后thread.start()调用以前使用ShutdownService建立一个Hook实例:


thread = new Thread(this, "Studio Teletype"); 

hook = shutdownService.createHook(thread); 

thread.start();


最后将while(true)替换为:


while (hook.keepRunning()) {


……通知线程何时须要结束while循环并关闭。


你可能已经注意到上面的代码里有一些System.out.println()调用。缘由并非对关闭hook线程的执行顺序不肯定。须要记住,不只仅是你编写的类试图关闭其余的子系统也是如此。这就是为这在我原来的代码中,logger.info(…)会给出下面的异常输出:


Exception in thread "Studio Teletype" java.lang.NoClassDefFoundError: org/apache/log4j/spi/ThrowableInformation

 at org.apache.log4j.spi.LoggingEvent.(LoggingEvent.java:159)

 at org.apache.log4j.Category.forcedLog(Category.java:391)

 at org.apache.log4j.Category.log(Category.java:856)

 at org.slf4j.impl.Log4jLoggerAdapter.info(Log4jLoggerAdapter.java:382)

 at com.captaindebug.longpoll.service.DeferredResultService.run(DeferredResultService.java:75)

 at java.lang.Thread.run(Thread.java:722)

Caused by: java.lang.ClassNotFoundException: org.apache.log4j.spi.ThrowableInformation

 at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1714)

 at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1559)

 ... 6 more


这里的异常是由于logger在调用时已经被卸载了,所以会给出报错。再一次,文档中是这么描述的:“Shutdown hook是在JVM生命周期中的一个微妙的时间执行,所以须要进行防护性变成。尤为是应该注意线程安全尽量地避免死锁。Hook代码应该不对任何服务盲目依赖,由于这些服务可能会注册本身的shutdown hook而且此时也在关闭的过程当中。例如,试图使用基于线程的服务好比AWT时间分发线程会致使死锁。


MatchReport类也须要进行相似的修改。主要的区别在于run() 方法中的hook.keepRunning()是一个for循环:


public class MatchReporter implements Runnable { 

 

  private static final Logger logger = LoggerFactory.getLogger(MatchReporter.class); 

 

  private final Match match; 

 

  private final Queue<Message> queue; 

 

  private volatile boolean start = true; 

 

  @Autowired

  private ShutdownService shutdownService; 

 

  private Hook hook; 

 

  public MatchReporter(Match theBigMatch, Queue<Message> queue) { 

    this.match = theBigMatch; 

    this.queue = queue; 

  } 

 

  /** 

   * 由Spring加载上下文以后调用。会启动匹配……

   */

  public void start() { 

 

    if (start) { 

      synchronized (this) { 

        if (start) { 

          start = false; 

          logger.info("Starting the Match Reporter..."); 

          String name = match.getName(); 

          Thread thread = new Thread(this, name); 

          hook = shutdownService.createHook(thread); 

 

          thread.start(); 

        } 

      } 

    } else { 

      logger.warn("Game already in progress"); 

    } 

  } 

 

  /** 

   * The main run loop 

   */

  @Override

  public void run() { 

 

    sleep(5); // Sleep等待应用加载

 

    logger.info("The match has now started..."); 

    long now = System.currentTimeMillis(); 

    List<Message> matchUpdates = match.getUpdates(); 

 

    for (Message message : matchUpdates) { 

 

      delayUntilNextUpdate(now, message.getTime()); 

      if (!hook.keepRunning()) { 

        break; 

      } 

      logger.info("Add message to queue: {}", message.getMessageText()); 

      queue.add(message); 

    } 

    start = true; // Game over, can restart 

    logger.warn("GAME OVER"); 

  } 

 

  private void sleep(int deplay) { 

    try { 

      TimeUnit.SECONDS.sleep(10); 

    } catch (InterruptedException e) { 

      logger.info("Sleep interrupted..."); 

    } 

  } 

 

  private void delayUntilNextUpdate(long now, long messageTime) { 

 

    while (System.currentTimeMillis() < now + messageTime) { 

 

      try { 

        Thread.sleep(100); 

      } catch (InterruptedException e) { 

        logger.info("MatchReporter Thread interrupted..."); 

      } 

    } 

  } 

 

}


最终的代码测试是在匹配更新过程到一半时执行Tomcat shutdown.sh命令。当JVM终止时会经过ShutdownDaemonHook类调用shutdown hook,其中的run()方法会对Hook实例列表循环执行通知他们关闭各自的线程。若是你执行tail -f查看服务器的日志文件(我这里是catalina.out,你的Tomcat配置可能与我不一样),你会看到服务器友好地关闭记录。


本文附带的代码能够在GitHub上找到:https://github.com/roghughe/captaindebug/tree/master/long-poll。

相关文章
相关标签/搜索