前往顾页
以后地位: 主页 > 精通Office > Ubuntu教程 >

Tomcat停机过程阐发与线程措置体例

时候:2018-02-11 00:14来源:知行网www.zhixing123.cn 编辑:麦田守望者

事情中常常碰到因为Tomcat shutdown时本身建立的线程没有及时停止而引发的各种莫名其妙的报错,这篇文章将经由过程对Tomcat停机过程的梳理,会商产生这些错误的启事,同时提出了两个可行的处理体例。

Tomcat停机过程阐发

一个Tomcat过程本质上是一个JVM过程,其外部布局以下图所示:

Tomcat

(图片来自收集)

从上至下别离为Server、service、connector | Engine、host、context。

在实现中,Engine和host只是一种笼统,更核心的服从在context中实现。顶层的Server只能有一个,一个Server可以包含多个Service,一个Service可以包含多个Connector和一个Continer。Continer是对Engine、Host或Context的笼统。不严格来讲,一个Context对应一个Webapp。

当Tomcat启动时,主线程的首要事情概括以下:

public void start() {

load();//config server and init it

getServer().start();//start server and all continers belong to it

Runtime.getRuntime().addShutdownHook(shutdownHook);// register the shutdown hook

await();//wait here util the end of Tomcat Proccess

stop();

}

  1. 经由过程扫描建设文件(默许为server.xml)来构建从顶层Server开端到Service、Connector等容器(此中还包含了对Context的构建)。
  2. 调用Catalina的start体例,进而调用Server的start体例。start体例将导致全部容器的启动。

Server、Service、Connector、Context等容器都实现了Lifecycle接口,同时这些组件保持了严格的、从上至下的树状布局。Tomcat只经由过程对根节点(Server)的生命周期办理便可以实现对所有树状布局中别的所有容器的办理。

  1. 将本身梗阻于await()体例。await()体例会等候一个收集连接请求,当有效户连接到对应端口并发送指定字符串(凡是是’SHUTDOWN’)时,await()前往,主线程继续履行。
  2. 主线程履行stop()体例。stop()体例将会从Server开端调用所有其下容器的stop体例。stop()体例履行完后,主线程加入,如果没有问题,Tomcat容器此时运行停止。

值得重视的是,stop()体例自Service下面一层开端是异行动行的。代码以下:

protected synchronized void stopInternal(){

/*other code*/

Container children[] = findChildren();

List<Future<Void>> results = new ArrayList<Future<Void>>();

for (int i = 0; i < children.length; i++) {

results.add(startStopExecutor.submit(new StopChild(children[i])));

}

boolean fail = false;

for (Future<Void> result : results) {

try {

result.get();

} catch (Exception e) {

log.error(sm.getString(“containerBase.threadedStopFailed”), e);

fail = true;

}

}

if (fail) {

throw new LifecycleException(

sm.getString(“containerBase.threadedStopFailed”));

}

/*other code*/

}

在这些被封闭的children中,遵循标准应当是Engine-Host-Context如许的层状布局,也就是说最后会调用Context的stop()体例。在Context的stopInternal体例中会调用这三个别例:

  • filterStop();
  • listenerStop();
  • ((Lifecycle) loader).stop();

(注:这只是此中的一部分,因为与我们阐发的过程有关所以列出来了,别的与过程无关的体例未予列出。)

此中filterStop会清理我们在web.xml中注册的filter,listenerStop会进一法度用web.xml中注册的Listener的onDestory体例(如果有多个Listener注册,调用依次与注册依次相反)。而loader在这儿是WebappClassLoader,此中首要的操纵(测验测验停止线程、清理援引资本和卸载Class)都是在stop函数中做的。

如果我们利用的SpringWeb,一般web.xml中注册的Listener将会是:

<listener>

<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>

</listener>

看ContextLoaderListener的代码不难发明,Spring框架经由过程Listener的contextInitialized体例初始化Bean,经由过程contextDestroyed体例清理Bean。

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

public ContextLoaderListener() {

}

public ContextLoaderListener(WebApplicationContext context) {

super(context);

}

public void contextInitialized(ServletContextEvent event) {

this.initWebApplicationContext(event.getServletContext());

}

public void contextDestroyed(ServletContextEvent event) {

this.closeWebApplicationContext(event.getServletContext());

ContextCleanupListener.cleanupAttributes(event.getServletContext());

}

}

在这儿有一个首要的事:我们的线程是在loader中被测验测验停止的,而loader的stop体例在listenerStop体例以后,也就是说,即便loader成功停止了用户本身启动的线程,仍然有可能在线程停止之前利用Sping框架,而此时Spring框架已在Listener中封闭了!何况在loader的清理线程过程中只需建设了clearReferencesStopThreads参数,用户本身启动的线程才会被强迫停止(利用Thread.stop()),而在年夜多数环境下,为了包管数据的完整性,这个参数不会被建设。也就是说,在WebApp中,用户本身启动的线程(包含Executors),都不会因为容器的加入而停止。

我们晓得,JVM自行加入的启事首要有两个:

  • 调用了System.exit()体例
  • 所有非保护线程都加入

而Tomcat中没有在stop履行结束时主动调用System.exit()体例,所以如果有效户启动的非保护线程,并且用户没有与容器同步封闭线程的话,Tomcat不会主动结束!这个问题临时搁置,下面说说停机时碰到的各种问题。

Tomcat停机过程中的异常阐发

IllegalStateException在利用Spring框架的Webapp中,Tomcat加入时Spring框架的封闭与用户线程结束之间有严峻的同步问题。在这段时候里(Spring框架封闭,用户线程结束前),会产生很多不成预感的问题。这些问题中最多见的就是IllegalStateException了。产生如许的异常时,标准代码以下:

 

public void run(){

while(!isInterrupted()) {

try {

Thread.sleep(1000);

GQBean bean = SpringContextHolder.getBean(GQBean.class);

/*do something with bean…*/

} catch (Exception e) {

e.printStackTrace();

}

}

}

这类错误很容易复现,也很常见,不消多说。

ClassNotFound/NullPointerException

这类错误不常见,阐发起来也比较费事。

在前面的阐发中我们肯定了两件事:

  1. 用户建立的线程不会跟着容器的烧毁而停止。
  2. ClassLoader在容器的停止过程中卸载了加载过的Class。

很容易肯定这又是由线程没有结束引发的。

  • 当ClassLoader卸载结束,用户线程测验测验去load一个Class时,报ClassNotFoundException或NoClassDefFoundError。
  • 在ClassLoader卸载过程中,因为Tomcat没有对停止容器进行严格的同步,此时如果测验测验load一个Class可能会导致NullPointerException,启事以下:

//part of load class code, may be executed in user thread

protected ResourceEntry findResourceInternal(…){

if (!started) return null;

synchronized (jarFiles) {

if (openJARs()) {

for (int i = 0; i < jarFiles.length; i++) {

jarEntry = jarFiles[i].getJarEntry(path);

if (jarEntry != null) {

try {

entry.manifest = jarFiles[i].getManifest();

} catch (IOException ioe) {

// Ignore

}

break;

}

}

}

}

/*Other statement*/

}

从代码中可以看到,对jarEntry的拜候进行了非常谨慎的同步操纵。在别的对jarEntry的利用处都有非常谨慎的同步,除在stop中没有:

// loader.stop() must be executed in stop thread

public void stop() throws LifecycleException {

/*other statement*/

length = jarFiles.length;

for (int i = 0; i < length; i++) {

try {

if (jarFiles[i] != null) {

jarFiles[i].close();

}

} catch (IOException e) {

// Ignore

}

jarFiles[i] = null;

}

/*other statement*/

}

可以看到,下面两段代码中,如果用户线程进入同步代码块后(此时会导致线程缓存区的革新),started变成false,跳过了更新jarFiles或此时jarFiles[0]还未被置空,比及从openJARs前往后,stop恰好履行过jarFiles[0] = null, 便会触发NullPointerException。

这个异常非常难以了解,启事就是为甚么会触发loadClass操纵,特别是在代码中并没有new一个类的时候。究竟上有很多时候都会触发对一个类的初始化查抄。(重视是类的初始化,不是类实例的初始化,二者天差地别)

以下环境将会触发类的初始化查抄:

  • 当火线程中第一次建立此类的实例
  • 当火线程中第一次调用类的静态体例
  • 当火线程中第一次利用类的静态成员
  • 当火线程中第一次为类静态成员赋值

(注:如果此时类已初始化结束,将直接前往,如果此时类还没有初始化,将履行类的初始化操纵)

当在一个线程中产生下面这些环境时就会触发初始化查抄(一个线程中最多查抄一次),查抄这个类的初始化环境之前必定需求获得这个类,此时需求调用loadClass体例。

一般有以下形式的代码容易触发上述异常:

try{

/**do something **/

}catch(Exception e){

//ExceptionUtil has never used in the current thread before

String = ExceptionUtil.getExceptionTrace(e);

//or this, ExceptionTracer never appears in the current thread before

System.out.println(new ExceptionTracer(e));

//or other statement that triggers a call of loadClass

/**do other thing**/

}

一些建议的措置体例

按照下面的阐发,造成异常的首要启事就是线程没有及时停止。所以处理体例的关头就是如安在容器停止之前,文雅地停止用户启动的线程。

建立本身的Listener作为停止线程的告诉者

按照阐发,项目中首要用到用户建立的线程,包含四种:

  • Thread
  • Executors
  • Timer
  • Scheduler

所以最直接的设法就是建立一种对这些组件的办理模块,详细做法分为两步:

  • 第一步:建立一个基于Listener的办理模块,并将下面提到的四种范例的类实例交由模块办理。
  • 第二步:在Listener监听到Tomcat停机时,触发其办理的实例对应的结束体例。比如Thread触发interrupt()体例,ExecutorService触发shutdown()或shutdownNow()体例(依靠详细战略挑选)等。

值得重视的是,对用户建立的Thread需求呼应Interrupt事件,即在isInterrupted()前往true或在捕获到InterruptException后,加入线程。究竟上,建立不呼应Interrupt事件的线程是一种非常不好的设想。

建立本身Listener的长处是可以主动在监听到事件时梗阻烧毁过程,为用户线程做清理事情争夺些时候,因为此时Spring还没有烧毁,法度的状况一切一般。

错误谬误就是对代码侵入性年夜,并且依靠于利用者的编码。

利用Spring供应的TaskExecutor

为了应对在webapp中办理本身线程的目标,Spring供应了一套TaskExcutor的东西。此中的ThreadPoolTaskExecutor与Java5中的ThreadPoolExecutor非常近似,只是生命周期会被Spring办理,Spring框架停止时,Executor也会被停止,用户线程会收到间断异常。同时,Spring还供应了ScheduledThreadPoolExecutor,对定时任务或要建立本身线程的需求可以用这个类。对线程办理,Spring供应了非常丰富的支撑,详细可以看这里:

https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#scheduling。

利用Spring框架的长处是对代码侵入性小,对代码依靠性也相对较小。

错误谬误是Spring框架不包管线程间断与Bean烧毁的时候前后依次,即如果一个线程在捕获InterruptException后,再经由过程Spring去getBean时,仍然会触发IllegalSateException。同时利用者仍然需求查抄线程状况或在Sleep中触发间断,不然线程仍然不会停止。

别的需求提示的

在下面的处理体例中,不管是在Listener中梗阻主线程的停止操纵,还是在Spring框架中不呼应interrupt状况,都能为线程继续做一些事情争夺些时候。但这个时候不是无穷的。在catalina.sh中,stop部分的脚本中我们可以看到(这里删繁就简表现一下):

#Tomcat停机脚本摘录

#第一次一般停止

eval “\”$_RUNJAVA\”” $LOGGING_MANAGER $JAVA_OPTS \

-Djava.endorsed.dirs=”\”$JAVA_ENDORSED_DIRS\”” -classpath “\”$CLASSPATH\”” \

-Dcatalina.base=”\”$CATALINA_BASE\”” \

-Dcatalina.home=”\”$CATALINA_HOME\”” \

-Djava.io.tmpdir=”\”$CATALINA_TMPDIR\”” \

org.apache.catalina.startup.Bootstrap “$@” stop

#如果停止失败 利用kill -15

if [ $? != 0 ]; then

kill -15 `cat “$CATALINA_PID”` >/dev/null 2>&1

#设置等候时候

SLEEP=5

if [ “$1” = “-force” ]; then

shift

#如果参数中有-force 将强迫停止

FORCE=1

fi

while [ $SLEEP -gt 0 ]; do

sleep 1

SLEEP=`expr $SLEEP – 1 `

done

#如果需求强迫停止 kill -9

if [ $FORCE -eq 1 ]; then

kill -9 $PID

fi

 

从下面的停止脚本可以看到,如果建设了强迫停止(我们办事器默许建设了),你梗阻停止过程去做本身的事的时候只需5秒钟。这期间另有别的线程在做一些任务和线程真正开端停止到发明停止的时候(比如从以后到下一次调用isInterrupted的时候),考虑到这些的话,最年夜梗阻时候应当更短。

从下面的阐发中也能够看到,如果办事中有比较首要又耗时的任务,又希望包管分歧性的话,最好的体例就是在梗阻的贵重的5秒钟时候里记录以后履行进度,比及办事重启的时候检测前次履行进度,然后从前次的进度中规复。

建议每个任务的履行粒度(两个isInterrupted的检测间隔)起码要节制在最年夜梗阻时候内,以留出充足时候做停止今后的记录事情。

参考质料

  • Tomcat源码7.0.69
  • Tomcat启动与停止办事道理http://blog.csdn.net/beliefer/article/details/51585006
  • Tomcat生命周期办理http://blog.csdn.net/beliefer/article/details/51473807
  • JVMs and kill signalshttp://journal.thobe.org/2013/02/jvms-and-kill-signals.html
  • Task Execution and Schedulinghttps://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#scheduling
  • 《Java并发编程的艺术》

作者:

 

高强,毕业于哈尔滨产业年夜学,目前在网易做着欢愉的背景开辟事情。

原文来自微信公家号:DBAplus社群

顶一下
(0)
0%
踩一下
(0)
0%
------分开线----------------------------
标签(Tag):tomcat Tomcat停机过程阐发
------分开线----------------------------
颁发评论
请自发遵循互联网相关的政策法规,严禁公布色情、暴力、革命的谈吐。
评价:
神色:
考证码:点击我更换图片
猜你感兴趣