现象
上线过程中出现调用tomcat的shutdown.sh 卡住导致tomcat无法停止的情况。
执行了tomcat的shutdown脚本后,java进程仍然存在。其实无法停止不是tomcat问题,是应用有问题,否则可以跑一个空tomcat,保证shutdown百分之百生效。
先说一下tomcat的大概关闭过程:
停止连接处理线程Accepter,停止接受新的请求
关闭tomcat自身的资源,例如各种service,连接器,protocol,container
然后tomcat主线程结束了,就是执行BootStrap这个类的线程
这是一个平滑关闭的过程,但是什么时候会导致所谓”tomcat无法关闭”?要更正一点,不是tomcat无法关闭,是执行tomcat的这个jvm无法关闭,这是本质的不同。
最根本的原因是:
当前运行tomcat的jvm里还有非deamon的线程没有结束执行,例如被阻塞,或者还在执行任务。这个现象就是tomcat 端口都已经关闭了,但是java进程还在。tomcat的停止只是结束了自己的线程,关闭了自己的资源。但是应用开启的非deamon线程,这个 tomcat是无能为力的。
资料
Java的线程分为两种:User Thread(用户线程)、DaemonThread(守护线程)。
只要当前JVM实例中尚存任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束是,守护线程随着JVM一同结束工作,Daemon作用是为其他线程提供便利服务,守护线程最典型的应用就是GC(垃圾回收器),他就是一个很称职的守护者。
User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。
过程
- 使用工具查看调用shutdown卡住状态的JVM状态:jvisualvm
- 选择线程tab 然后点击dump线程


如图:
守护线程会在线程名之后有daemon标识,那么找到现在没有daemon标识的线程:pool-2-thread-1,可以看到来源是一个ScheduledThreadPoolExecutor,说明我们在项目代码中用到了线程池。且由于线程池将守护线程转换成用户线程,导致tomcat无法停止(因为还有用户线程在运行中)
解决
- 不使用线程池--这不科学
- 避免线程池将守护线程转换成用户线程
package com.lagou.pard.agent.boot;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class DefaultNamedThreadFactory implements ThreadFactory {
private static final AtomicInteger BOOT_SERVICE_SEQ = new AtomicInteger(0);
private final AtomicInteger threadSeq = new AtomicInteger(0);
private final String namePrefix;
public DefaultNamedThreadFactory(String name) {
namePrefix = "XXX-" + BOOT_SERVICE_SEQ.incrementAndGet() + "-" + name + "-";
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r,namePrefix + threadSeq.getAndIncrement());
t.setDaemon(true);
return t;
}
}
然后在初始化地方使用:
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(new DefaultNamedThreadFactory("Name"));
其实最重要的一句就是t.setDaemon(true);
,设置守护线程。
参考:http://www.importnew.com/26834.html