从Tomcat无法正常关闭讲讲Java线程关闭问题【转载】
正常情况下,会优先采用catalina.sh stop来停止Tomcat实例,这样可以让服务有机会处理完请求,并做好善后工作。 但如果通过catalina.sh stop命令无法关闭Tomcat实例,则只能kill -9了。
为什么在给Tomcat发出stop命令以后,Tomcat实例无法关闭?
可能有两种原因:
- Tomcat的主线程没有结束(也即main函数没有执行结束);
- Tomcat中启动的webapps有非daemon线程阻止了Tomcat进程的关闭;
第一种情况,如果发出stop命令以后,Tomcat主线程并没有结束,自然通过它启动的webapps也是无法关闭的。虽然可以确信Tomcat不可能有这个问题,但还是拉出Tomcat的关闭过程代码看看:
- public void start() {
- if (getServer() == null) {
- load();
- }
- if (getServer() == null) {
- log.fatal("Cannot start server. Server instance is not configured.");
- return;
- }
- long t1 = System.nanoTime();
- // Start the new server
- try {
- getServer().start();
- } catch (LifecycleException e) {
- log.fatal(sm.getString("catalina.serverStartFail"), e);
- try {
- getServer().destroy();
- } catch (LifecycleException e1) {
- log.debug("destroy() failed for failed Server ", e1);
- }
- return;
- }
- ……//省略
- if (await) {
- await();
- stop();
- }
- }
首先需要清楚,Tomcat的正常关闭是通过socket发送命令的方式来触发的,Tomcat在启动完成以后会通过await()一直等待,知道接收到shutdown命令后退出,执行后面的stop()关闭Tomcat实例。
此后不会再有任何await,所以说Tomcat主线程是会正常关闭的。如果不相信,可以开启jpda debug一下,我就这么干了。
既然第一种可能不存在,那只能是webapps中有非daemon线程没有正常关闭了。为什么会这样?因为非Daemon线程被认为是工作线程,必须要主动关闭,而daemon线程属于后台线程,在非daemon线程关闭以后,daemon线程会自动关闭,典型的以main函数为入口的主线程便是非daemon的工作线程。
一个示例:
- public static void main(String[] args) {
- Thread thread = new Thread(() -> {
- while(true){
- LockSupport.parkNanos(1000 * 1000 * 3);
- }
- });
- thread.setDaemon(true);
- thread.start();
- try {
- Thread.sleep(1000 * 3);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
这里启动了一个非daemon线程,所以即使主线程执行完成以后,应用还是不会正常关闭,如果把线程改成daemon则会。
基于这个原理可以开始定位为什么Tomcat示例无法关闭了,可以通过jstack看看还有那些线程还在运行,之后逐一排除。
最终发现是Java的线程池引起的,用Executors new了一个线程池,因为默认情况下,Executors使用了它自己的默认ThreadFactory,这个东西有毒,它new出来的线程是这样的:
- public Thread newThread(Runnable r) {
- Thread t = new Thread(group, r,
- namePrefix + threadNumber.getAndIncrement(),
- 0);
- if (t.isDaemon())
- t.setDaemon(false);
- if (t.getPriority() != Thread.NORM_PRIORITY)
- t.setPriority(Thread.NORM_PRIORITY);
- return t;
- }
这些线程都是非daemon的,所以通过这个线程池submit了任务以后,如果不主动调用线程池的shutdown()函数是无法destroy这些线程的。