jedis:commons-pool-evictor-thread线程不能自动关闭?

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关闭。
在这里插入图片描述
现在有两个问题:

  1. commons-pool-evictor-threadcommons-pool-EvictionTimer线程是做什么用的?
  2. 为什么在这两个程序中evictor线程的类型居然不一样?

带着这两个问题我开始分析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-poolGenericObjectPool类的初始化

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的版本号退回到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.javaclose()方法实现

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,问题可以彻底解决。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

10km

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值