前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >八、HikariCP源码分析之ConcurrentBag一

八、HikariCP源码分析之ConcurrentBag一

原创
作者头像
用户1422411
发布于 2022-06-25 09:55:13
发布于 2022-06-25 09:55:13
8510
举报

欢迎访问我的博客,同步更新: 枫山别院

源代码版本2.4.5-SNAPSHOT

大家好,今天我们一起分析下 HikariCP 的核心ConcurrentBag,它是管理连接池的最重要的核心类。从它的名字大家可以看得出来,它是一个并发管理类,性能非常好,这是它性能甩其他连接池十条街的秘密所在。

代码概览

我们先看一下代码,注意这不是全部的代码,省略了不太重要的部分。大家可以看到我加了非常详细的注释,对详解不太感兴趣的朋友可以直接读一下代码即可,不过这部分历时好几个夜晚我才写完,大家可以稍稍捧个场:

代码语言:java
AI代码解释
复制
//可用连接同步器, 用于线程间空闲连接数的通知, synchronizer.currentSequence()方法可以获取当前数量
//其实就是一个计数器, 连接池中创建了一个连接或者还回了一个连接就 + 1, 但是连接池的连接被借走, 是不会 -1 的, 只加不减
//用于在线程从连接池中获取连接时, 查询是否有空闲连接添加到连接池, 详见borrow方法
private final QueuedSequenceSynchronizer synchronizer;
//sharedList保存了所有的连接
private final CopyOnWriteArrayList<T> sharedList;
//threadList可能会保存sharedList中连接的引用
private final ThreadLocal<List<Object>> threadList;
//对HikariPool的引用, 用于请求创建新连接
private final IBagStateListener listener;
//当前等待获取连接的线程数
private final AtomicInteger waiters;
//标记连接池是否关闭的状态
private volatile boolean closed;


/**
 * 该方法会从连接池中获取连接, 如果没有连接可用, 会一直等待timeout超时
 *
 * @param timeout  超时时间
 * @param timeUnit 时间单位
 * @return a borrowed instance from the bag or null if a timeout occurs
 * @throws InterruptedException if interrupted while waiting
 */
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException {
   //①
   //先尝试从ThreadLocal中获取
   List<Object> list = threadList.get();
   if (weakThreadLocals && list == null) {
      //如果ThreadLocal是 null, 就初始化, 防止后面 npe
      list = new ArrayList<>(16);
      threadList.set(list);
   }
   //②
   //如果ThreadLocal中有连接的话, 就遍历, 尝试获取
   //从后往前反向遍历是有好处的, 因为最后一次使用的连接, 空闲的可能性比较大, 之前的连接可能会被其他线程偷窃走了
   for (int i = list.size() - 1; i >= 0; i--) {
      final Object entry = list.remove(i);
      @SuppressWarnings("unchecked") final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
      if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
         return bagEntry;
      }
   }
   //③
   //如果没有从ThreadLocal中获取到连接, 那么就sharedList连接池中遍历, 获取连接, timeout时间后超时
   //因为ThreadLocal中保存的连接是当前线程使用过的, 才会在ThreadLocal中保留引用, 连接池中可能还有其他空闲的连接, 所以要遍历连接池
   //看一下requite(final T bagEntry)方法的实现, 还回去的连接放到了ThreadLocal中
   timeout = timeUnit.toNanos(timeout);
   Future<Boolean> addItemFuture = null;
   //记录从连接池获取连接的开始时间, 后面用
   final long startScan = System.nanoTime();
   final long originTimeout = timeout;
   long startSeq;
   //将等待连接的线程计数器加 1
   waiters.incrementAndGet();
   try {
      do {
         // scan the shared list
         do {
            //④
            //当前连接池中的连接数, 在连接池中添加新连接的时候, 该值会增加
            startSeq = synchronizer.currentSequence();
            for (T bagEntry : sharedList) {
               if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
                  // if we might have stolen another thread's new connection, restart the add...
                  //⑤
                  //如果waiters大于 1, 说明除了当前线程之外, 还有其他线程在等待空闲连接
                  //这里, 当前线程的addItemFuture是 null, 说明自己没有请求创建新连接, 但是拿到了连接, 这就说明是拿到了其他线程请求创建的连接, 这就是所谓的偷窃了其他线程的连接, 然后当前线程请求创建一个新连接, 补偿给其他线程
                  if (waiters.get() > 1 && addItemFuture == null) {
                     //提交一个异步添加新连接的任务
                     listener.addBagItem();
                  }
                  return bagEntry;
               }
            }
         } while (startSeq < synchronizer.currentSequence()); //如果连接池中的空闲连接数量比循环之前多了, 说明有新连接加入, 继续循环获取
         //⑥
         //循环完一遍连接池(也可能循环多次, 如果正好在第一次循环完连接池后有新连接加入, 那么会继续循环), 还是没有能拿到空闲连接, 就请求创建新的连接
         if (addItemFuture == null || addItemFuture.isDone()) {
            addItemFuture = listener.addBagItem();
         }
         //计算 剩余的超时时间 = 用户设置的connectionTimeout - (系统当前时间 - 开始获取连接的时间_代码①处 即从连接池中获取连接一共使用的时间)
         timeout = originTimeout - (System.nanoTime() - startScan);
      } while (timeout > 10_000L && synchronizer.waitUntilSequenceExceeded(startSeq, timeout)); //③
      //⑦
      //这里的循环条件比较复杂
      //1. 如果剩余的超时时间, 大于10_000纳秒
      //2. startSeq的数量, 即空闲连接数超过循环之前的数量
      //3. 没有超过超时时间timeout
      //满足以上 3 个条件才会继续循环, 否则阻塞线程, 直到满足以上条件
      //如果一直等到timeout超时时间用完都没有满足条件, 结束阻塞, 往下走
      //有可能会动态改变的条件, 只有startSeq数量改变, 是②处添加的创建连接请求
   } finally {
      waiters.decrementAndGet();
   }

   return null;
}

/**
 * 该方法将借出去的连接还回到连接池中
 * 不通过该方法还回的连接会造成内存泄露
 *
 * @param bagEntry the value to return to the bag
 * @throws NullPointerException  if value is null
 * @throws IllegalStateException if the requited value was not borrowed from the bag
 */
public void requite(final T bagEntry) {
   //⑧
   //lazySet方法不能保证连接会立刻被设置成可用状态, 这是个延迟方法
   //这是一种优化, 如果要立即生效的话, 可能会需要使用volatile等, 让其他线程立即发现, 这会降低性能, 使用lazySet浪费不了多少时间, 但是不会浪费性能
   bagEntry.lazySet(STATE_NOT_IN_USE);

   //⑨
   //将连接放回到threadLocal中
   final List<Object> threadLocalList = threadList.get();
   if (threadLocalList != null) {
      threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
   }
   //通知等待线程, 有可用连接
   synchronizer.signal();
}

/**
 * 在连接池中添加一个连接
 * 新连接都是添加到sharedList中, threadList是sharedList中的部分连接的引用
 *
 * @param bagEntry an object to add to the bag
 */
public void add(final T bagEntry) {
   if (closed) {
      LOGGER.info("ConcurrentBag has been closed, ignoring add()");
      throw new IllegalStateException("ConcurrentBag has been closed, ignoring add()");
   }
   //⑩
   sharedList.add(bagEntry);
   synchronizer.signal();
}

/**
 * 从连接池中移除一个连接.
 * 这个方法只能用于从<code>borrow(long, TimeUnit)</code> 或者 <code>reserve(T)</code>方法中获取到的连接
 * 也就是说, 这个方法只能移除处于使用中和保留状态的连接
 *
 * @param bagEntry the value to remove
 * @return true if the entry was removed, false otherwise
 * @throws IllegalStateException if an attempt is made to remove an object
 *                               from the bag that was not borrowed or reserved first
 */
public boolean remove(final T bagEntry) {
   //⑪
   //尝试标记移除使用中和保留状态的连接, 如果标记失败, 就是空闲的连接, 直接返回 false
   //也就是检查连接的状态, 不能移除空闲的连接或者已经标记移除的连接
   if (!bagEntry.compareAndSet(STATE_IN_USE, STATE_REMOVED) && !bagEntry.compareAndSet(STATE_RESERVED, STATE_REMOVED) && !closed) {
      LOGGER.warn("Attempt to remove an object from the bag that was not borrowed or reserved: {}", bagEntry);
      return false;
   }
   //如果上面标记成功了, 那么从连接池中移除这个连接
   final boolean removed = sharedList.remove(bagEntry);
   if (!removed && !closed) {
      LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry);
   }

   // synchronizer.signal();
   return removed;
}

上面的代码是ConcurrentBag中的成员变量和最重要的四个方法,ConcurrentBag中的属性我们穿插在代码中解释。

borrow方法

borrow方法应该是整个 HikariCP 中最最核心的方法,它是我们从连接池中获取连接的时候最终会调用到的方法,一切秘密都在这里了。我们分析下:

①ThreadLocal

代码语言:java
AI代码解释
复制
//①
//先尝试从ThreadLocal中获取
List<Object> list = threadList.get();
if (weakThreadLocals && list == null) {
  //如果ThreadLocal是 null, 就初始化, 防止后面 npe
  list = new ArrayList<>(16);
  threadList.set(list);
}

threadList是在ConcurrentBag上的成员变量,它的定义是private final ThreadLocal<List> threadList;,可见它是一个ThreadLocal,也就是每个线程都有独享的一个 List,它是用于保存当前线程用过的连接。注意这里我说的是"用过",不是所有的连接。因为还有一个成员变量private final CopyOnWriteArrayList<T> sharedList;,它是真正的保存所有的连接的地方,它是一个CopyOnWriteArrayList,在写入的时候会先复制一个 sharedList2,然后修改这个新的 sharedList2,最后将变量地址指向新的sharedList2,不是直接写入当前的sharedList,典型的空间换时间的一个做法,可以避免写入前要锁住sharedList,从而导致降低性能。

HikariCP 使用过的连接,在还回连接池的时候,是直接放在了ThreadLocal中。说到这里,可能会有同学问了:sharedList中保存了所有的连接,当用户借走了一个连接,不是应该把这个连接从sharedList中移除,然后还回来的时候再把连接加入到sharedList中?为什么还回去的时候,没有放到sharedList中呢?

首先,明确一点,HikariCP 不是这样做的。为什么呢?如果用户借用连接的时候,你从sharedList中移除了,那么相当于这个连接脱离了 HikariCP 的管理,后面 HikariCP 还怎么管理这个连接呢?比如这个连接的生命周期到时间了,连接都让用户拐跑了,我还怎么关闭这个连接呢?所以,所有的连接都不能脱离掌控,一个都不能少。其实,我们在sharedList中保存的仅仅是数据库连接的引用,这些连接是所有的线程都可见的,各个线程也可以随意保存连接的引用,只是要使用的时候必须要走borrow方法,按流程来。

为什么要放到线程的threadList中?

因为下次获取的时候比较方便,也许会提高性能。每个线程都优先从自己的本地线程中拿,竞争的可能性大大降低啊,也许这个连接刚刚用完到再次获取的时间极短,这个连接很可能还空闲着。只有在本地线程中的连接都不能使用的时候,才去sharedList这个 HikariCP的总仓库里获取。

举一个生活例子:假如你是一个连锁店老板,提供汽车出租服务,有一个总仓库,所有的连锁店都从这里提车出租给用户。刚开始,你是每租一辆车都去仓库直接提货,用户还车的时候,你直接送到仓库。过了一段时间,你觉得这样不行啊,太浪费时间了,而且所有的连锁店都这样,各个店的老板都去提车,太忙了,还得排队。要不用户还回来的车先放店里吧,这样下次有用户租车就不用去仓库了,直接给他,方便很多,店里没车了再去总仓提车。其他连锁店都开始这么搞,大家都先用店里的车不够再去总仓。生意火爆,有一天店里没车了,你去仓库提车,仓库管理员说:仓库也没车了,天通苑的连锁店里有闲着的,你去那里提吧,于是你把天通苑连锁店的车借走了。所以各个连锁店之间也有相互借车。

例子可能不太恰当,一时也想不到同样道理的生活例子,但是就这个意思。HikariCP 也是这样,用户使用的连接,还回连接池的时候,直接放到线程的本地threadList中,如果用户又要借用连接,先看本地有没有,优先使用本地连接,只有本地没有或者都不可用的时候,再去 HikariCP 的连接池里获取。但是跟借车不同,因为我们本地是保存的sharedList中连接的引用,虽然你还有这个连接的引用,但是很可能它已经被其他线程从sharedList借走了,这就是HikariCP所谓的线程间的连接窃取。所以线程在本地的threadList就算拿到了连接,也必须检查下状态,是不是可用的。

说到这里,还没有解析代码,扯远了。①处代码就是先从本地的threadList里取出连接的 List,然后检查下List 是否为空,是空的直接初始化一个 List,因为下面要用到,防止抛空指针了。大家可以看到判空的时候,还有一个条件是weakThreadLocals,这个标识是表示threadList是否是弱引用。如果是弱引用,那么很可能 GC 的时候会被回收掉,所以变成 null 了,但是如果不是弱引用的话,那么它是在初始化ConcurrentBag的时候,就是一个FastList了,不用担心是 null。那么什么情况下threadList会是弱引用呢?当 HikariCP 运行在容器中时,会使用弱引用,因为在容器重新部署的时候,可能会导致发成内存泄露,具体大家可以看下#39 的 issue。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
十、HikariCP源码分析之ConcurrentBag三
一般我们都是通过 Spring 来使用 HikariCP 的,自己动手启动一个连接池的机会还是少。在 Spring 中使用非常方便,一切都是 Spring 帮我们搞定,我们只管使用,所以需要将连接还回连接池的机会也比较少,也有可能你是间接用过,比如从 HikariCP 中借用的连接,用完之后调用了 close方法,连接其实并没有真正的被关闭,而是还回了连接池,真正的close方法被 HikariCP 重写了。其实这是一个至关重要的方法,如果借用出去的连接,不通过这个方法还回来,会导致内存泄露的。
用户1422411
2022/06/25
4490
九、HikariCP源码分析之ConcurrentBag二
OK,我们继续。这里遍历的 list 变量,是threadList,是当前线程使用过的连接,保存在本地线程的引用。
用户1422411
2022/06/25
4130
【追光者系列】HikariCP 源码分析之 allowPoolSuspension
摘要: 原创出处 https://mp.weixin.qq.com/s/-WGg22lUQU41c_8lx6kyQA 「渣渣王子」欢迎转载,保留摘要,
芋道源码
2018/07/31
1.2K0
基于HiKariCP组件,分析连接池原理
HiKariCP作为SpringBoot2框架的默认连接池,号称是跑的最快的连接池,数据库连接池与之前两篇提到的线程池和对象池,从设计的原理上都是基于池化思想,只是在实现方式上有各自的特点;首先还是看HiKariCP用法的基础案例:
知了一笑
2022/04/18
8630
基于HiKariCP组件,分析连接池原理
【追光者系列】HikariCP源码分析之ConcurrentBag
HikariCP contains a custom lock-free collection called a ConcurrentBag. The idea was borrowed from the C# .NET ConcurrentBag class, but the internal implementation quite different. The ConcurrentBag provides…
用户1655470
2018/07/24
1.3K0
【追光者系列】HikariCP源码分析之ConcurrentBag
读懂HikariCP一百行代码,多线程就是个孙子!
通常,我在看书的时候一般不写代码,因为我的脑袋被设定成单线程的,一旦同时喂给它不同的信息,它就无法处理。
xjjdog
2022/12/22
4970
读懂HikariCP一百行代码,多线程就是个孙子!
干掉Druid,HakariCP 为什么这么快?
Springboot 2.0将 HikariCP 作为默认数据库连接池这一事件之后,HikariCP 作为一个后起之秀出现在大众的视野中。HikariCP 是在日本的程序员开源的,hikari日语意思为“光”,HikariCP 也以速度快的特点受到越来越多人的青睐。
码猿技术专栏
2024/01/29
2800
干掉Druid,HakariCP 为什么这么快?
【追光者系列】HikariCP源码分析之evict、时钟回拨、连接创建生命周期
evict定义在com.zaxxer.hikari.pool.PoolEntry中,evict的汉语意思是驱逐、逐出,用来标记连接池中的连接不可用。
用户1655470
2018/07/24
2.9K0
【追光者系列】HikariCP源码分析之evict、时钟回拨、连接创建生命周期
【源码分析】SpringBoot2中取代Druid的超级连接池:HikariCP之ConcurrentBag
HiKariCP是数据库连接池的一个后起之秀,号称性能最好,可以完美地PK掉其他连接池。
Java_老男孩
2020/07/27
1.2K0
【追光者系列】HikariCP源码分析之故障检测那些思考 fail fast &amp; allowPoolSuspension
由于时间原因,本文主要内容参考了 https://segmentfault.com/a/1190000013136251,并结合一些思考做了增注。
用户1655470
2018/07/24
1.4K0
SpringBoot官方为什么采用这个数据库连接池?史上最快?
现在已经有很多公司在使用HikariCP了,HikariCP还成为了SpringBoot默认的连接池,伴随着SpringBoot和微服务,HikariCP 必将迎来广泛的普及。
macrozheng
2021/07/27
9610
SpringBoot官方为什么采用这个数据库连接池?史上最快?
MySQL 连接挂死了!该如何排查?
近期由测试反馈的问题有点多,其中关于系统可靠性测试提出的问题令人感到头疼,一来这类问题有时候属于“偶发”现象,难以在环境上快速复现;二来则是可靠性问题的定位链条有时候变得很长,极端情况下可能要从 A 服务追踪到 Z 服务,或者是从应用代码追溯到硬件层面。
程序员小富
2022/12/10
3.4K0
MySQL 连接挂死了!该如何排查?
十一、HikariCP源码分析之HouseKeeper
HouseKeeper是一个HikariPool的内部类,它实现了Runnable接口,也就是一个线程任务。这个任务是由ScheduledThreadPoolExecutor类型的线程池执行的,也就是说它是一个定时任务。我们在《HikariCP源码分析之初始化分析二》中分析 HikariCP 初始化的时候,遇到了houseKeepingExecutorService的初始化,简单分析了它的初始化过程,但是这个任务是非常重要的,我们要仔细分析一下。
用户1422411
2022/06/25
1.6K0
HikariPool一直报连接不可用
一开始发现测试环境报错,原先配置6现在配置20依然还是很频繁的报错,想看下底层到底如何处理的导致这个问题,到底什么情况。
查拉图斯特拉说
2023/10/25
9890
HikariPool一直报连接不可用
二、HikariCP获取连接流程源码分析二
在上一篇《HikariCP获取连接流程源码分析一》中,我们分析了HikariDataSource的getConnection()方法,而这个方法,其实详细的实现细节都是在HikariPool的getConnection()方法中,我们来分析下HikariPool的getConnection()方法。
用户1422411
2022/06/25
8320
java应用最好的数据源 Hikari?
DBCP是Apache推出的数据库连接池(Database Connection Pool)。
冯杰宁
2019/12/16
2.8K0
java应用最好的数据源 Hikari?
三、HikariCP获取连接流程源码分析三
这里涉及到 HikariCP 的一个设计点,HikariCP的连接不是实时从连接池里剔除的,只是给连接上打个标记而已,都是在获取连接的时候检查是否可用,如果不可用的时候才直接从连接池里删除。如果在 HikariCP的任何地方都可能剔除连接,那么剔除连接的地方会比较多,会很乱,也容易引发 bug。反之,把剔除链接的操作收缩到某几个固定的逻辑中,就比较好管理。
用户1422411
2022/06/25
1.1K0
Java 数据持久化系列之 HikariCP (一)
在上一篇《Java 数据持久化系列之池化技术》中,我们了解了池化技术,并使用 Apache-common-Pool2 实现了一个简单连接池,实验对比了它和 HikariCP、Druid 等数据库连接池的性能数据。在性能方面,HikariCP遥遥领先,而且它还是 Spring Boot 2.0 默认的数据库连接池。下面我们就来了解一下这款明星级开源数据库连接池的实现。
程序员历小冰
2020/04/14
1.2K0
Java 数据持久化系列之 HikariCP (一)
五、HikariCP源码分析之初始化分析二
在上一节,我们说到了pool = fastPathPool = new HikariPool(this);中的new HikariPool(this)。我们来看下代码:
用户1422411
2022/06/25
7000
【追光者系列】HikariCP连接池监控指标实战
业务方关注哪些数据库指标? 首先分享一下自己之前的一段笔记(找不到引用出处了) 系统中多少个线程在进行与数据库有关的工作?其中,而多少个线程正在执行 SQL 语句?这可以让我们评估数据库是不是系统瓶颈。 多少个线程在等待获取数据库连接?获取数据库连接需要的平均时长是多少?数据库连接池是否已经不能满足业务模块需求?如果存在获取数据库连接较慢,如大于 100ms,则可能说明配置的数据库连接数不足,或存在连接泄漏问题。 哪些线程正在执行 SQL 语句?执行了的 SQL 语句是什么?数据库中是否存在系统瓶颈或已经
芋道源码
2018/06/13
6.5K0
推荐阅读
相关推荐
十、HikariCP源码分析之ConcurrentBag三
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档