写点什么

九、HikariCP 源码分析之 ConcurrentBag 二

作者:阿白
  • 2022 年 7 月 29 日
  • 本文字数:4391 字

    阅读完需:约 14 分钟

九、HikariCP源码分析之ConcurrentBag二

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


源代码版本 2.4.5-SNAPSHOT

②检查本地保存的连接

//②//如果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;  }}
复制代码


OK,我们继续。这里遍历的 list 变量,是threadList,是当前线程使用过的连接,保存在本地线程的引用。


可以看到这个遍历的顺序,是倒序遍历的。list 里的连接,肯定是最后一个是最后放进去的,也就是最近使用过的,这个连接还可以继续使用的可能极大,时间越早的连接,就越可能被其他线程借用走了,所以这就是为什么要倒序遍历,我们要先检查能使用的可能性最大的连接。说到这里,我们在平时的业务代码中,要用 if 检查一些条件,这时候我们要有意识的先检查可能性最大的条件,这有利于减少判断的次数,提高程序的性能。


在从 list 中获取连接的时候,使用的是remove方法,也就是说,无论如何,这个连接的引用我们不在本地保存了,如果它可以用,那么用完之后它又会加入到本地的threadList,如果不能使用了,那么我们就删除了这个无用的连接引用。


下面第 三句代码其实就是类型的强转,忽略之。有意思的是接下来第四句的判断,直接在 if 中就执行修改连接状态的操作。每个连接都有一个状态,它的类型是AtomicInteger,是个数字,并且是原子操作,线程安全。compareAndSet方法执行的时候,将STATE_NOT_IN_USE状态跟连接的当前状态对比,一样的时候才将它修改成STATE_IN_USE,既保证了线程安全,又保证了只有在连接是空闲状态才能使用线程,不会错误使用了其他状态的连接。此时,如果状态修改成功了,那么直接将该连接返回给用户使用。


说到这里,应该说一下连接有哪些状态。

连接的状态

  • STATE_NOT_IN_USE = 0 //空闲状态, 可以被借走

  • STATE_IN_USE = 1 //使用中状态, 已经被借走, 正在使用中

  • STATE_REMOVED = -1 //已删除状态, 不能借走

  • STATE_RESERVED = -2 //保留状态, 不能借走


一共四种状态,只有在连接空闲的时候才能被借走,其他三个状态都不行。值得一提的是STATE_RESERVED状态,它是在连接池挂起时的一个状态,如果不知道连接池挂起,大家可以看下《HikariCP 源码分析之获取连接流程二》。

③准备工作

//③//如果没有从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;//将等待连接的线程计数器加 1waiters.incrementAndGet();
复制代码


上面说到了从本地的threadList中获取连接,如果拿不到的话,那么就要走到这里了。这里是获取连接前的准备工作。


  • timeout是获取连接的超时时间,这个是作为参数传入的,转换为纳秒可以提高精度。

  • startScan是记录开始获取连接的起始时间,用于后面计算还剩下多少时间的。

  • originTimeout就等于timeout,因为timeout代表剩余时间,后面的计算中会变的,所以赋值给originTimeout,记住原始的值是多少。

  • startSeq用于记录当期 synchronizer 的值,它是判断是否有可用连接加入连接池的。每当有一个可用连接加入,synchronizer 就会加 1,我们只要记下 synchronizer 的当前值,然后一段时间后比较 synchronizer 最新值,如果 synchronizer 变大了,就说明有新连接加入了。

  • waiters是等待中的线程数,是记录有多少线程在等待获取连接的计数器。此处将计数器加 1。


其实上面代码都是一些用于记录原始值的,没什么好说的。


我们稍微说说System.nanoTime()吧,跟System.currentTimeMillis()非常像的方法,都是获取时间的。但是System.currentTimeMillis()获取的是系统时间的毫秒数,而System.nanoTime()获取的并不是系统时间的纳秒数,这个很多同学可能一直误会了这个方法。


JDK 提供的System.currentTimeMillis()方法其实获取的时间并不准确,因为可能会受到时间校准的影响,而System.nanoTime()返回当前 JVM 的高精度时间,该方法只能用来测量时段而和系统时间无关,它的返回值可能是从某个时间点开始计算的,可能是负数,只能用于计算时间差,不能用于系统时间相关的逻辑。


所以,在做性能测试的时候,统计时间,使用System.nanoTime()比较好。Netty 中的时间轮就是使用这个,调系统时间不会导致触发任务。

④⑤添加连接任务

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()); //如果连接池中的空闲连接数量比循环之前多了, 说明有新连接加入, 继续循环获取
复制代码


④和⑤我们还是一起分析比较好。上面我们已经说过了startSeq,略。


sharedList是整个 HikariCP 的连接池,里面保存了所有的连接,终于,在这里进入主题了。


我们遍历整个连接池,尝试获取连接,在 if 的compareAndSet中一个一个尝试修改它的状态,如果修改成功了,说明我们拿到了这个连接的使用权,Good!本来应该直接 return 这个连接给用户就可以了吧?为什么还要判断???


在上面一篇文章中,我们举例租车的时候,提到过,线程间的连接是会相互窃取的,其实那个窃取不算是真的窃取,因为虽然你本地保存了连接的引用,但是连接又不是你创建的,其他线程也可以从连接池里拿,没有毛病。


但是这里是真的窃取。我们判断waiters.get() > 1 && addItemFuture == nullwaiters是当前正在等待获取连接的线程数,这个我们说过了。它大于 1,说明除了当前线程自己在等之外,还有别的线程也在等连接呢;addItemFuture代表创建连接的任务,它是 null 的话,说明当前线程自己没有创建过创建新连接的任务。但是呢,我居然拿到连接了,你说运气好不好!巧了呀!既然这个连接不是我们创建的,那肯定是别的线程创建的呀,我们偷来了,这咋整呢,要不我们补偿一个给它吧。于是,我们执行listener.addBagItem();请求创建一个新连接补偿给别的线程,别让人一直等了!


如果 for 循环执行完了,还是没有拿到连接呢?这个 for 循环是 do-while 循环中嵌套的 for 循环,for 循环执行完了一遍,就说明整个连接池我们查找了一遍,没有拿到连接。那么 do-while 要不要继续执行,要看条件了对吧?


这里startSeq < synchronizer.currentSequence()startSeq是我们循环之前记录的连接数量,synchronizer.currentSequence()是当前的连接数量,如果现在比之前的数量大了,说明有新的可用连接加入了连接池,就可以继续执行 for 循环遍历连接池。


如果startSeq < synchronizer.currentSequence()不成立,说明我们在执行 for 循环期间,没有新连接加入连接池。

⑥请求创建连接

//⑥//循环完一遍连接池(也可能循环多次, 如果正好在第一次循环完连接池后有新连接加入, 那么会继续循环), 还是没有能拿到空闲连接, 就请求创建新的连接if (addItemFuture == null || addItemFuture.isDone()) {addItemFuture = listener.addBagItem();}//计算 剩余的超时时间 = 用户设置的connectionTimeout - (系统当前时间 - 开始获取连接的时间_代码①处 即从连接池中获取连接一共使用的时间)timeout = originTimeout - (System.nanoTime() - startScan);} while (timeout > 10_000L && synchronizer.waitUntilSequenceExceeded(startSeq, timeout)); //③
复制代码


上面的代码是不完整的,少了一个 do,在最外层,不影响我们分析。此处的循环是一个三层的嵌套,两个 do-while,里面再嵌套一个 for 循环,很绕。


上面我们说到了,遍历完整个连接池之后,也没有新连接加入连接池,for 循环和里层的 do-while 就执行完了。此时是真的没有连接可用了,怎么办,创建一个连接呗,然后执行如下的 if 判断addItemFuture == null || addItemFuture.isDone()addItemFuture == null说明没有创建过创建连接的任务,那么addItemFuture.isDone()是啥?这是说明它创建过创建连接的任务,任务执行完了,悲催的是,我们还是没有拿到创建的连接,肯定是让哪个天杀的线程偷了,没有天理了呀!没办法,我们执行addItemFuture = listener.addBagItem();再创建一个连接。


此时,还有最外层的一个 do-while 循环,它的判断条件是什么?我们好像还一直没有检查超时时间吧?没错,就是这个了。timeout = originTimeout - (System.nanoTime() - startScan);计算一下,我们获取连接还剩余多少时间了。


timeout > 10_000L && synchronizer.waitUntilSequenceExceeded(startSeq, timeout)这个条件有点小复杂。


  • timeout > 10_000L表示剩余时间至少要大于 10000 纳秒,太少了说不定还没有执行一次 for 循环就到时间了。

  • synchronizer.waitUntilSequenceExceeded(startSeq, timeout)这个条件比较神奇,如果在timeout时间内,synchronizer的值大于startSeq,就返回 true。


综合一下,最外层的 do-while 要执行的话,必须剩余超时时间timeout大于 10000 纳秒,同时,在timeout时间用尽之前,必须有新连接加入连接池,我们才能继续执行循环,否则一直阻塞,直到条件达成或者时间用尽。

最终结果

代码还剩一个 finally 了。


} finally {      waiters.decrementAndGet();   }
return null;
复制代码


borrow 方法的最后几行代码,直到线程前面用尽最后的时间,也没有获取到连接,最后,不能再等了,从等待线程的计数器中把自己减去,直接返回 null 给用户吧,尽力了......

发布于: 2 小时前阅读数: 12
用户头像

阿白

关注

爱生活,爱代码 2020.03.25 加入

有两样东西,越是经常而持久地对它们进行反复思考,它们就越是使心灵充满常新而日益增长的惊赞和敬畏:我头上的星空和我心中的道德法则——康德

评论

发布
暂无评论
九、HikariCP源码分析之ConcurrentBag二_数据库_阿白_InfoQ写作社区