Session清理
Background 线程
前面我们分析了 Session 的创建过程,而 Session 会话是有时效性的,下面我们来看下 tomcat 是如何进行失效检查的。在分析之前,我们先回顾下 Container
容器的 Background 线程。
tomcat 所有容器组件,都是继承至 ContainerBase
的,包括 StandardEngine
、StandardHost
、StandardContext
、StandardWrapper
,而 ContainerBase
在启动的时候,如果 backgroundProcessorDelay
参数大于 0 则会开启 ContainerBackgroundProcessor
后台线程,调用自己以及子容器的 backgroundProcess
进行一些后台逻辑的处理,和 Lifecycle
一样,这个动作是具有传递性的,也就是说子容器还会把这个动作传递给自己的子容器,如下图所示,其中父容器会遍历所有的子容器并调用其 backgroundProcess
方法,而 StandardContext
重写了该方法,它会调用 StandardManager#backgroundProcess()
进而完成 Session 的清理工作。看到这里,不得不感慨 tomcat 的责任
关键代码如下所示:
ContainerBase.java(省略了异常处理代码)
protected synchronized void startInternal() throws LifecycleException {
// other code......
// 开启ContainerBackgroundProcessor线程用于处理子容器,默认情况下backgroundProcessorDelay=-1,不会启用该线程
threadStart();
}
protected class ContainerBackgroundProcessor implements Runnable {
public void run() {
// threadDone 是 volatile 变量,由外面的容器控制
while (!threadDone) {
try {
Thread.sleep(backgroundProcessorDelay * 1000L);
} catch (InterruptedException e) {
// Ignore
}
if (!threadDone) {
processChildren(ContainerBase.this);
}
}
}
protected void processChildren(Container container) {
container.backgroundProcess();
Container[] children = container.findChildren();
for (int i = 0; i < children.length; i++) {
// 如果子容器的 backgroundProcessorDelay 参数小于0,则递归处理子容器
// 因为如果该值大于0,说明子容器自己开启了线程处理,因此父容器不需要再做处理
if (children[i].getBackgroundProcessorDelay() <= 0) {
processChildren(children[i]);
}
}
}
Session 检查
backgroundProcessorDelay
参数默认值为 -1
,单位为秒,即默认不启用后台线程,而 tomcat 的 Container 容器需要开启线程处理一些后台任务,比如监听 jsp 变更、tomcat 配置变动、Session 过期等等,因此 StandardEngine
在构造方法中便将 backgroundProcessorDelay
参数设为 10(当然可以在 server.xml
中指定该参数),即每隔 10s 执行一次。那么这个线程怎么控制生命周期呢?我们注意到 ContainerBase
有个 threadDone
变量,用 volatile
修饰,如果调用 Container 容器的 stop 方法该值便会赋值为 false,那么该后台线程也会退出循环,从而结束生命周期。另外,有个地方需要注意下,父容器在处理子容器的后台任务时,需要判断子容器的 backgroundProcessorDelay
值,只有当其小于等于 0 才进行处理,因为如果该值大于0,子容器自己会开启线程自行处理,这时候父容器就不需要再做处理了
前面分析了容器的后台线程是如何调度的,下面我们重点来看看 webapp 这一层,以及 StandardManager
是如何清理过期会话的。StandardContext
重写了 backgroundProcess
方法,除了对子容器进行处理之外,还会对一些缓存信息进行清理,关键代码如下所示:
StandardContext.java
@Override
public void backgroundProcess() {
if (!getState().isAvailable())
return;
// 热加载 class,或者 jsp
Loader loader = getLoader();
if (loader != null) {
loader.backgroundProcess();
}
// 清理过期Session
Manager manager = getManager();
if (manager != null) {
manager.backgroundProcess();
}
// 清理资源文件的缓存
WebResourceRoot resources = getResources();
if (resources != null) {
resources.backgroundProcess();
}
// 清理对象或class信息缓存
InstanceManager instanceManager = getInstanceManager();
if (instanceManager instanceof DefaultInstanceManager) {
((DefaultInstanceManager)instanceManager).backgroundProcess();
}
// 调用子容器的 backgroundProcess 任务
super.backgroundProcess();
StandardContext
重写了 backgroundProcess
方法,在调用子容器的后台任务之前,还会调用 Loader
、Manager
、WebResourceRoot
、InstanceManager
的后台任务,这里我们只关心 Manager
的后台任务。弄清楚了 StandardManager
的来龙去脉之后,我们接下来分析下具体的逻辑。
StandardManager
继承至 ManagerBase
,它实现了主要的逻辑,关于 Session 清理的代码如下所示。backgroundProcess 默认是每隔10s调用一次,但是在 ManagerBase
做了取模处理,默认情况下是 60s 进行一次 Session 清理。tomcat 对 Session 的清理并没有引入时间轮,因为对 Session 的时效性要求没有那么精确,而且除了通知 SessionListener
。
ManagerBase.java
public void backgroundProcess() {
// processExpiresFrequency 默认值为 6,而backgroundProcess默认每隔10s调用一次,也就是说除了任务执行的耗时,每隔 60s 执行一次
count = (count + 1) % processExpiresFrequency;
if (count == 0) // 默认每隔 60s 执行一次 Session 清理
processExpires();
}
/**
* 单线程处理,不存在线程安全问题
*/
public void processExpires() {
long timeNow = System.currentTimeMillis();
Session sessions[] = findSessions(); // 获取所有的 Session
int expireHere = 0 ;
for (int i = 0; i < sessions.length; i++) {
// Session 的过期是在 isValid() 里面处理的
if (sessions[i]!=null && !sessions[i].isValid()) {
expireHere++;
}
}
long timeEnd = System.currentTimeMillis();
// 记录下处理时间
processingTime += ( timeEnd - timeNow );