`

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

阅读更多

链接:http://www.importnew.com/6255.html

 

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

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

ps -ef | grep java

先得到进程pid,然后

kill -9 <<pid>>

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

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

@Override
public void run() {
 
  while (true) {
    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

 

原文链接: javacodegeeks 翻译: ImportNew.com - 唐尤华
译文链接: http://www.importnew.com/6255.html
分享到:
评论

相关推荐

    tomcat进程不能完全关闭的处理办法.txt

    tomcat shutdown后,进程还存在linux系统中的解决办法

    Linux下Tomcat的启动、关闭、杀死进程

    在Linux环境下部署和管理Java Web应用程序时,Apache Tomcat作为一款广泛使用的应用服务器,其操作过程对于系统管理员和开发人员来说至关重要。本文将详细介绍如何在Linux环境下启动、关闭以及强制终止Tomcat服务。 ...

    JAVA虚拟机关闭钩子(Shutdown Hook).docx

    JAVA虚拟机关闭钩子(Shutdown Hook).docx

    Linux下Tomcat重新启动

    我们可以使用 ./shutdown.sh 命令来关闭 Tomcat。 但是,在关闭 Tomcat 之前,我们需要检查 Tomcat 是否已经关闭。我们可以使用 ps 命令来检查 Tomcat 的进程。具体来说,我们可以使用 ps -ef|grep java 命令来检查...

    强制关闭tomcat sh脚本

    有时,由于各种原因,如应用程序挂起、内存溢出或其他系统问题,普通的`shutdown.sh`命令可能无法正常关闭Tomcat,这时就需要使用强制关闭的脚本来结束Tomcat进程。 首先,让我们了解一下Tomcat的常规关闭流程。在...

    tomcat启动和关闭

    标题中的“Tomcat启动和关闭”是指在Apache Tomcat服务器中进行服务的启停操作,这是每个Java Web开发者必备的基础技能。Apache Tomcat是一个开源的Servlet容器,它实现了Java Servlet和JavaServer Pages(JSP)规范...

    Linux下关闭tomcat

    推荐首选使用Shutdown脚本来优雅地关闭Tomcat,这样可以确保资源得到正确释放,并减少数据丢失的风险。而在紧急情况下或者无法通过正常方式关闭时,可以考虑使用Kill命令强制关闭。无论采用哪种方式,都应确保在操作...

    解析Runtime中shutdown hook的使用详解

    在Java编程中,`Runtime`类的`...在实际开发中,合理使用`shutdown hook`可以提高程序的健壮性和资源管理效率。然而,考虑到其潜在的风险和限制,应当谨慎使用,并确保清理任务能够在程序正常退出时也能顺利完成。

    解决Tomcat使用shutdown.bat关闭会将其他Tomcat关掉的问题

    在使用Apache Tomcat服务器时,有时可能会遇到一个棘手的问题:当你尝试通过`shutdown.bat`脚本关闭一个Tomcat实例时,它意外地关闭了同一台机器上的其他Tomcat实例。这个问题主要是由于Tomcat的批处理脚本如何查找...

    java实现后台进程

    在Java编程中,后台进程(Background Process)通常指的是在主应用程序执行时,不与用户界面直接交互,而是默默地运行在后台执行特定任务的程序部分。这些任务可能包括数据处理、定时任务、服务监控等。Java提供了...

    tomcat启动管理工具

    2. **停止Tomcat**:对应地,使用`bin/shutdown.sh`或`bin/shutdown.bat`可以安全关闭Tomcat。这将发送一个信号让所有正在运行的请求完成,然后优雅地关闭服务器。 3. **监控Tomcat**:`bin/catalina.sh`或`...

    自动启动Tomcat_C#_tomcat_

    标题中的“自动启动Tomcat”指的是在服务器上配置Tomcat服务,使其能够在关闭后自动重新启动,以确保应用程序的连续性和稳定性。C#是.NET框架的一部分,通常用于编写Windows服务或者控制台应用来实现这样的自动化...

    tomcat的使用方法

    以下是一份详细的Tomcat使用教程,旨在帮助你快速上手。 ### 1. 安装Tomcat 首先,你需要从Apache官方网站(http://tomcat.apache.org/)下载最新版本的Tomcat。下载的是一个ZIP文件,解压到你选择的目录,例如`C:...

    tomcat压缩包解压后使用

    10. **停止Tomcat**:执行`CATALINA_HOME/bin/shutdown.sh`(Linux/Mac)或`shutdown.bat`(Windows)来关闭Tomcat服务。 以上就是解压Apache Tomcat 9.0.68后使用的基本步骤和注意事项。在实际开发和运维过程中,...

    tomcat使用心得

    * 服务端口配置:port属性指定了Tomcat监听的端口号,shutdown属性指定了关闭服务器的命令字符串。 * 连接器配置:Connector元素用于定义Tomcat的连接器,包括名称、端口号、协议类型、最大线程数、超时时间等属性。...

    apache-tomcat-7.0.82.tar.gz 【Tomcat服务器,Tomcat7 Linux版】

    4. 使用`bin/startup.sh`启动Tomcat,`bin/shutdown.sh`关闭Tomcat。 5. 配置防火墙或者SELinux,允许必要的端口(默认为8080)通过。 6. 测试Tomcat是否正常工作,访问`http://your_server_ip:8080`,如果能看到...

    apache-tomcat-7.0.39

    - `bin`:包含Tomcat的可执行文件和脚本,如startup.sh/startup.bat用于启动Tomcat,shutdown.sh/shutdown.bat用于关闭Tomcat。 - `conf`:存放所有配置文件,如server.xml、web.xml、context.xml等。 - `lib`:存储...

    window和linux通用版Tomcat

    在Windows上,通常通过bin目录下的startup.bat和shutdown.bat文件来启动和关闭Tomcat;在Linux中,可以使用bin目录下的startup.sh和shutdown.sh脚本。 **安全管理** 无论是Windows还是Linux,安全管理都是运行...

    Tomcat8.0.46bin和src免费下载

    其中,`startup.sh` 和 `shutdown.sh` 是在Unix/Linux系统中启动和关闭Tomcat的脚本,而在Windows系统中对应的则是`startup.bat` 和 `shutdown.bat`。此外,`catalina.sh` 和 `catalina.bat` 文件提供了更多的控制...

Global site tag (gtag.js) - Google Analytics