版权声明:本文为博主原创文章,转载请注明源地址。 https://cloud.tencent.com/developer/article/1433436
jedis为了管理网络连接使用了apache commons-pool用于连接资源管理,我使用jedis版本是2.8.2,依赖的commons-pool 版本是2.4.2
之前我的项目中调用jedis时,都是添加ShutdownHook在程序结束时自动关闭JedisPool.就是类似下面的代码:
static{
// 程序退出时自动销毁连接池对象
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run() {
closeAll();
}});
}
/**
* 关闭并删除所有资源池中的{@link JedisPool}实例
*/
public synchronized static void closeAll(){
for(Iterator<JedisPool> itor = POOL_SET.iterator();itor.hasNext();){
JedisPool p = itor.next();
itor.remove();
p.close();
}
}
这样比较方便,就是应用层对JedisPool只管用就行了,可以不需要主动去执行关闭动作。
然而,最近在一个新项目中使用jedis时,我还是照以往的方式,没有去主动关闭JedisPool,让程序结束时自动关闭。却出了问题:程序没有正常关闭,如下图,可以看到除了守护线程外,有一个名为commons-pool-evictor-thread
的线程还在运行,导致程序无法退出。
为什么会这样的?
对比我之前的程序,我发现了不一样的地方,如下图是能够正常关闭的一个测试程序的线程运行情况,可以看到有一个名为commons-pool-EvictionTimer
的线程,但与上图不同的是,这个线程是守护线程。我们知道JVM不需要等守护线程结束就可以结束。所以这个commons-pool-EvictionTimer
守护线程不会影响JVM关闭。
现在有两个问题:
commons-pool-evictor-thread
或commons-pool-EvictionTimer
线程是做什么用的?带着这两个问题我开始分析jedis的源码,首先解决第一个问题.
commons-pool-evictor-thread
线程通过分析jedis和google搜索,大概搞明白commons-pool-evictor-thread
线程的作用,这是commons-pool产生的线程,evictor的英文解释是“驱逐者”,所以在这里commons-pool-evictor-thread
是一个定时执行的任务的线程,用于定期从资源池中删除空闲不用的资源对象。用在JedisPool中就是定期删除空闲的Jedis对象。
commons-pool-evictor-thread
线程的类型居然不一样?下面是redis.clients.util.Pool
类的代码片段,从代码中可以到jedis对commons-pool
的GenericObjectPool
类的初始化
public abstract class Pool<T> implements Closeable {
protected GenericObjectPool<T> internalPool;
/**
* Using this constructor means you have to set and initialize the internalPool yourself.
*/
public Pool() {
}
public Pool(final GenericObjectPoolConfig poolConfig, PooledObjectFactory<T> factory) {
initPool(poolConfig, factory);
}
// ......
// 初始化资源池
public void initPool(final GenericObjectPoolConfig poolConfig, PooledObjectFactory<T> factory) {
if (this.internalPool != null) {
try {
closeInternalPool();
} catch (Exception e) {
}
}
this.internalPool = new GenericObjectPool<T>(factory, poolConfig);
}
// .....
}
在commons-pool 2.4.2版本的EvictionTimer.java代码中可以找到创建commons-pool-EvictionTimer
线程的代码
/**
* {@link PrivilegedAction} used to create a new Timer. Creating the timer
* with a privileged action means the associated Thread does not inherit the
* current access control context. In a container environment, inheriting
* the current access control context is likely to result in retaining a
* reference to the thread context class loader which would be a memory
* leak.
*/
private static class PrivilegedNewEvictionTimer implements PrivilegedAction<Timer> {
/**
* {@inheritDoc}
*/
@Override
public Timer run() {
// 创建定时器线程,isDaemon为true,指定为守护线程
return new Timer("commons-pool-EvictionTimer", true);
}
}
所以我们可以理解使用commons-pool 2.4.2版本,commons-pool-EvictionTimer
线程因为是守护线程所以不影响JVM关闭。
但两个项目中Eviction的名字不一样(commons-pool-EvictionTimer
vs commons-pool-evictor-thread
),让我意识到,我可能使用了不同的commons-pool版本。检查maven的dependency hierarchy
果然发现了问题,如下红线标出的因为与项目中其他模块依赖commons-pool的版本号冲突,所以这里commons-pool的版本自动升级到2.5.0:
在2.5.0的EvictionTimer.java代码中创建evicition线程的代码是这样的:
/**
* Thread factory that creates a thread, with the context classloader from this class.
*/
private static class EvictorThreadFactory implements ThreadFactory {
@Override
public Thread newThread(final Runnable r) {
// 创建commons-pool-evictor-thread线程,但没有指定为守护线程
final Thread t = new Thread(null, r, "commons-pool-evictor-thread");
AccessController.doPrivileged(new PrivilegedAction<Void>() {
@Override
public Void run() {
t.setContextClassLoader(EvictorThreadFactory.class.getClassLoader());
return null;
}
});
return t;
}
}
上面的代码中创建commons-pool-evictor-thread
线程,但没有指定为守护线程所以这个线程不会自动结束导致程序不能正常退出.
好了,到这里问题的原因总算是找到了。那么 怎么解决commons-pool-evictor-thread
线程导致的程序不能结束的问题呢?
想办法让commons-pool的版本号退回到2.4.2这个版本,不要使用高于此版本的commons-pool.因为目前所有高于此版本的common2-pool版本创建的commons-pool-evictor-thread
线程都不是守护线程。
JedisPool#close()
方法造成commons-pool-evictor-thread
线程没有被关闭的原因就是没有执行GenericObjectPool#close()
方法,
所以在程序结束时显式执行JedisPool#close()
方法,执行JedisPool#close()
方法会自动关闭内部使用的GenericObjectPool
实例。
下面是下面是redis.clients.util.Pool
类的代码片段
public abstract class Pool<T> implements Closeable {
// 内部的资源池对象
protected GenericObjectPool<T> internalPool;
/**
* Using this constructor means you have to set and initialize the internalPool yourself.
*/
public Pool() {
}
@Override
public void close() {
destroy();
}
public void destroy() {
closeInternalPool();
}
protected void closeInternalPool() {
try {
// 调用GenericObjectPool#close()方法
internalPool.close();
} catch (Exception e) {
throw new JedisException("Could not destroy the pool", e);
}
}
}
下面则是GenericObjectPool.java的close()
方法实现
public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
implements ObjectPool<T>, GenericObjectPoolMXBean, UsageTracking<T> {
// .........
/**
* Closes the pool. Once the pool is closed, {@link #borrowObject()} will
* fail with IllegalStateException, but {@link #returnObject(Object)} and
* {@link #invalidateObject(Object)} will continue to work, with returned
* objects destroyed on return.
* <p>
* Destroys idle instances in the pool by invoking {@link #clear()}.
*/
@Override
public void close() {
if (isClosed()) {
return;
}
synchronized (closeLock) {
if (isClosed()) {
return;
}
// 资源池关闭之前先停止Evictor线程
startEvictor(-1L);
closed = true;
// This clear removes any idle objects
clear();
jmxUnregister();
// Release any threads that were waiting for an object
idleObjects.interuptTakeWaiters();
}
}
// .....
}
evictor线程设置为守护线程显然更方便应用程序的设计,但为什么2.4.2以后的版本启动Evictor线程不再是守护线程?在我看来这应该是个bug。
在commons-pool上星期(2019/3/30)提的一次交中显示这个问题已经被修复了,参见pull request: https://github.com/apache/commons-pool/pull/20
下面是最新版本的EvictionTimer.java中创建commons-pool-evictor-thread
线程的代码片段:
/**
* Thread factory that creates a daemon thread, with the context class loader from this class.
*/
private static class EvictorThreadFactory implements ThreadFactory {
@Override
public Thread newThread(final Runnable runnable) {
final Thread thread = new Thread(null, runnable, "commons-pool-evictor-thread");
// commons-pool-evictor-thread设置为守护线程,这就是唯一的修改
thread.setDaemon(true); // POOL-363 - Required for applications using Runtime.addShutdownHook(). --joshlandin 03.27.2019
AccessController.doPrivileged(new PrivilegedAction<Void>() {
@Override
public Void run() {
thread.setContextClassLoader(EvictorThreadFactory.class.getClassLoader());
return null;
}
});
return thread;
}
}
所以commons-pool下一个release版本就会包含这个PR,问题可以彻底解决。