Redian新闻
>
读懂HikariCP一百行代码,多线程就是个孙子!

读懂HikariCP一百行代码,多线程就是个孙子!

公众号新闻

点击上方“芋道源码”,选择“设为星标

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 
来源:小姐姐味道

总结:Java届很难得有读百十行代码就能增加修炼的机会,这里有一个。

通常,我在看书的时候一般不写代码,因为我的脑袋被设定成单线程的,一旦同时喂给它不同的信息,它就无法处理。

但多线程对电脑来说就是小菜一碟,它可以同时做很多事,看起来匪夷所思。好希望把自己的大脑皮层移植到这些牛x的设备上。

用人脑思考电脑正在思考的问题,这本身就是一种折磨。但平常的工作和面试中,又不得不面对这样的场景,所以多线程就成了编程路上一块难啃的骨头。

HikariCP是SpringBoot默认的数据库连接池,它毫不谦虚的的起了一个叫做的名字,这让国产Druid很没面子。

还是言归正传,看一下Hikari中的ConcurrentBag吧。

核心数据结构

多线程代码一个让人比较头疼的问题,就是每个API我都懂,但就是不会用。很多对concurrent包倒背如流的同学,在面对现实的问题时,到最后依然不得不被迫加上Lock或者synchronized。

ConcurrentBag是一个Lock free的数据结构,主要用作数据库连接的存储,可以说整个HikariCP的核心就是它。删掉乱七八糟的注释和异常处理,可以说关键的代码也就百十来行,但里面的道道却非常的多。

ConcurrentBag速度很快,要达到这个目标,就需要一定的核心数据结构支持。

private final CopyOnWriteArrayList<T> sharedList;
private final ThreadLocal<List<Object>> threadList;
private final AtomicInteger waiters;
private final SynchronousQueue<T> handoffQueue;
  • sharedList 用来缓存所有的连接,是一个CopyOnWriteArrayList结构。
  • threadList 用来缓存某个线程所使用的所有连接,相当于快速引用,是一个ThreadLocal类型的ArrayList。
  • waiters 当前正在获取连接的等待者数量。AtomicInteger,就是一个自增对象。当waiters的数量大于0时候,意味着有线程正在获取资源。
  • handoffQueue 0容量的快速传递队列,SynchronousQueue类型的队列,非常有用。

ConcurrentBag里面的元素,为了能够无锁化操作,需要使用一些变量来标识现在处于的状态。抽象的接口如下:

public interface IConcurrentBagEntry{
    int STATE_NOT_IN_USE = 0;
    int STATE_IN_USE = 1;
    int STATE_REMOVED = -1;
    int STATE_RESERVED = -2;

    boolean compareAndSet(int expectState, int newState);
    void setState(int newState);
    int getState();
}

有了这些数据结构的支持,我们的ConcurrentBag就可以实现它光的宣称了。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

获取连接

连接的获取是borrow方法,还可以传入一个timeout作为超时控制。

public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException

首先,如果某个线程执行非常快,使用了比较多的连接,就可以使用ThreadLocal的方式快速获取连接对象,而不用跑到大池子里面去获取。代码如下。

// Try the thread-local list first
final var list = threadList.get();
for (int i = list.size() - 1; i >= 0; i--) {
    final var entry = list.remove(i);
    final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
    if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
        return bagEntry;
    }
}

我们都知道,包括ArrayList和HashMap一些基础的结构,都是Fail Fast的,如果你在遍历的时候,删掉一些数据,有可能会引起问题。幸运的是,由于我们的List是从ThreadLocal获取的,它首先就避免了线程安全的问题。

接下来就是遍历。这段代码采用的是尾遍历(头遍历会出现错误),用于快速的从列表中找到一个可以复用的对象,然后使用CAS来把状态置为使用中。但如果对象正在被使用,则直接删除它。

在ConcurrentBag里,每个ThreadLocal最多缓存50个连接对象引用。

当ThreadLocal里找不到可复用的对象,它就会到大池子里去拿。也就是下面这段代码。

// Otherwise, scan the shared list ... then poll the handoff queue
final int waiting = waiters.incrementAndGet();
try {
   for (T bagEntry : sharedList) {
      if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
         // If we may have stolen another waiter's connection, request another bag add.
         if (waiting > 1) {
            listener.addBagItem(waiting - 1);
         }
         return bagEntry;
      }
   }

   listener.addBagItem(waiting);
   
   // 还拿不到,就需要等待别人释放了
   timeout = timeUnit.toNanos(timeout);
   do {
      final var start = currentTime();
      final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
      if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
         return bagEntry;
      }

      timeout -= elapsedNanos(start);
   } while (timeout > 10_000);

   return null;
}
finally {
   waiters.decrementAndGet();
}

首先要注意,这段代码可能是由不同的线程执行的,所以必须要考虑线程安全问题。由于shardList是线程安全的CopyOnWriteArrayList,适合读多写少的场景,我们可以直接进行遍历。

这段代码的目的是一样的,需要从sharedList找到一个空闲的连接对象。这里把自增的waiting变量传递到外面的代码进行处理,主要是由于想要根据waiting的大小来确定是否创建新的对象。

如果无法从池子里获取连接,则需要等待别的线程释放一些资源。

创建对象的过程是异步的,要想获取它,还需要依赖一段循环代码。while循环代码是纳秒精度,会尝试从handoffQueue里获取。最终会调用SynchronousQueue的transfer方法。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

归还连接

有借就有还,当某个连接使用完毕,它将被归还到池子中。

public void requite(final T bagEntry)
{
   bagEntry.setState(STATE_NOT_IN_USE);

   for (var i = 0; waiters.get() > 0; i++) {
      if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
         return;
      }
      else if ((i & 0xff) == 0xff) {
         parkNanos(MICROSECONDS.toNanos(10));
      }
      else {
         Thread.yield();
      }
   }

   final var threadLocalList = threadList.get();
   if (threadLocalList.size() < 50) {
      threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
   }
}

首先,把这个对象置为可用状态。然后,代码会进入一个循环,等待使用方把这个连接接手过去。当连接处于STATE_NOT_IN_USE状态,或者队列中的数据被取走了,那么就可以直接返回了。

由于waiters.get()是实时获取的,有可能长时间一直大于0,这样代码就会变成死循环,浪费CPU。代码会尝试不同层次的睡眠,一个是每隔255个waiter睡10ns,一个是使用yield让出cpu时间片。

如果归还连接的时候并没有被其他线程获取到,那么最后我们会把归还的连接放入到相对应的ThreadLocal里,因为对一个连接来说,借和还,通常是一个线程。

知识点

看起来平平无奇的几行代码,为什么搞懂了就能Hold住大部分的并发编程场景呢?主要还是这里面的知识点太多。下面我简单罗列一下,你可以逐个攻破。

  1. 使用ThreadLocal来缓存本地资源引用,使用线程封闭的资源来减少锁的冲突
  2. 采用读多写少的线程安全的CopyOnWriteArrayList来缓存所有对象,几乎不影响读取效率
  3. 使用基于CAS的AtomicInteger来计算等待者的数量,无锁操作使得计算更加快速
  4. 0容量的交换队列SynchronousQueue,使得对象传递更加迅速
  5. 采用compareAndSet的CAS原语来控制状态的变更,安全且效率高。很多核心代码都是这么设计的
  6. 在循环中使用park、yield等方法,避免死循环占用大量CPU
  7. 需要了解并发数据结构中的offer、poll、peek、put、take、add、remove方法的区别,并灵活应用
  8. CAS在设置状态时,采用了volatile关键字修饰,对于volatile的使用也是一个常见的优化点
  9. 需要了解WeakReference弱引用在垃圾回收时候的表现

麻雀虽小,五脏俱全。如果你想要你的多线程编程能力更上一层楼,读一读这个短小精悍的ConcurrentBag吧。当你掌握了它,多线程的那些东西,不过是小菜一碟。



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

已在知识星球更新源码解析如下:

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
爆肝50道面试题,彻底帮你搞定多线程面试!Spring 多线程异步上传图片、处理水印、缩略图海马体黄逸涵:你做事的过程就是信仰丨新新访谈一行代码12倍加速Bert推理,OpenAI编程语言加持的引擎火了黎舒苇听《归途有风》加 3 行代码减少 80% 构建时间探索你奶奶个孙子仅花200行代码,如何将60万行的RocksDB改造成协程PyTorch 2.0 来了!100% 向后兼容,一行代码将训练提速 76%重写 50 万行代码,从 0 自研的云原生 HSTAP 能否成为数据库的未来?| Q推荐2023医美揭秘第一弹!医美内部员工最爱做的护肤疗程就是它!灾难!多伦多华人亲历机场崩溃:传送带断裂!数百行李箱堆积!新来个阿里 P7,仅花 2 小时,撸出一个多线程永动任务,看完直接跪了,真牛逼!在北京,几行代码实现看房自由!秋游河溪--看不够的 Erindale 公园网站都变成灰色,几行代码就搞定了!在美国北方,荷花(莲藕)怎么过冬?瞧不上 C++ 和 D 语言,国外程序员将 5.8 万行代码迁移到 Jai 语言,到底图什么?Java批量更新太慢?多线程+List分段完美解决!通俗大白话,彻底弄懂https原理本质PyTorch 2.0来了!100%向后兼容,一行代码将训练提速76%!3行代码建模,训练速度提升200%?这款时序开源神器PaddleTS太强了!苹果市值暴涨1761美元,马斯克要求推特程序员书面打印代码,上海推出新版禁烟标识,“斯人”版教材找到了,这就是今天的其它大新闻!离婚的亢奋《悠悠岁月》(5)MyBatis引起的线程池线程打满问题排查过程0行代码拿210万年薪,ChatGPT催生新型「程序员」岗:工作纯靠和AI聊天君权与神权 信仰的颠覆(六十)【友情转发】MITCSSA年度巨献|一行代码,告别光棍节从JVM虚拟机到多线程,手撸Java开发面试必备技术栈 | 极客时间线程池中线程抛了异常,该如何处理?用1个月重构了同事写的烂代码,我总结出了15条重写烂代码的经验!三行代码解决长尾不平衡类别分类!间隔校准算法Margin Calibration来了!几行代码就能价值千万美金?丨1024程序员节支付宝:多线程事务怎么回滚?说用 @Transactional 可以回去等通知了!退休后你打算带孙子,带孙子,还是带孙子?
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。