0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

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

jf_ro2CN3Fa 来源:芋道源码 作者:芋道源码 2022-12-14 15:45 次阅读


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

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

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

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

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

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

核心数据结构

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

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

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

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

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

publicinterfaceIConcurrentBagEntry{
intSTATE_NOT_IN_USE=0;
intSTATE_IN_USE=1;
intSTATE_REMOVED=-1;
intSTATE_RESERVED=-2;

booleancompareAndSet(intexpectState,intnewState);
voidsetState(intnewState);
intgetState();
}

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

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

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

获取连接

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

publicTborrow(longtimeout,finalTimeUnittimeUnit)throwsInterruptedException

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

//Trythethread-locallistfirst
finalvarlist=threadList.get();
for(inti=list.size()-1;i>=0;i--){
finalvarentry=list.remove(i);
finalTbagEntry=weakThreadLocals?((WeakReference)entry).get():(T)entry;
if(bagEntry!=null&&bagEntry.compareAndSet(STATE_NOT_IN_USE,STATE_IN_USE)){
returnbagEntry;
}
}

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

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

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

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

//Otherwise,scanthesharedlist...thenpollthehandoffqueue
finalintwaiting=waiters.incrementAndGet();
try{
for(TbagEntry:sharedList){
if(bagEntry.compareAndSet(STATE_NOT_IN_USE,STATE_IN_USE)){
//Ifwemayhavestolenanotherwaiter'sconnection,requestanotherbagadd.
if(waiting>1){
listener.addBagItem(waiting-1);
}
returnbagEntry;
}
}

listener.addBagItem(waiting);

//还拿不到,就需要等待别人释放了
timeout=timeUnit.toNanos(timeout);
do{
finalvarstart=currentTime();
finalTbagEntry=handoffQueue.poll(timeout,NANOSECONDS);
if(bagEntry==null||bagEntry.compareAndSet(STATE_NOT_IN_USE,STATE_IN_USE)){
returnbagEntry;
}

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

returnnull;
}
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/

归还连接

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

publicvoidrequite(finalTbagEntry)
{
bagEntry.setState(STATE_NOT_IN_USE);

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

finalvarthreadLocalList=threadList.get();
if(threadLocalList.size()< 50){
threadLocalList.add(weakThreadLocals?newWeakReference<>(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吧。当你掌握了它,多线程的那些东西,不过是小菜一碟。

审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • cpu
    cpu
    +关注

    关注

    68

    文章

    10733

    浏览量

    209899
  • 数据库
    +关注

    关注

    7

    文章

    3718

    浏览量

    64088
  • 多线程
    +关注

    关注

    0

    文章

    275

    浏览量

    19863

原文标题:读懂HikariCP一百行代码,多线程就是个孙子!

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    多线程设计模式到对 CompletableFuture 的应用

    最近在开发 延保服务 频道页时,为了提高查询效率,使用到了多线程技术。为了对多线程方案设计有更加充分的了解,在业余时间读完了《图解 Java 多线程设计模式》这本书,觉得收获良多。本篇文章将介绍其中
    的头像 发表于 06-26 14:18 195次阅读
    从<b class='flag-5'>多线程</b>设计模式到对 CompletableFuture 的应用

    bootloader开多线程做引导程序,跳app初始化后直接进hardfualt,为什么?

    如标题,想做一远程升级的项目,bootloader引导区域和app都是开多线程跑的,就是自己写了小的任务调度器,没什么功能主要是想让程序快速的响应,延时不会对其他程序造成堵塞,程序
    发表于 04-18 06:07

    java实现多线程的几种方式

    Java实现多线程的几种方式 多线程是指程序中包含了两或以上的线程,每个线程都可以并行执行不同的任务或操作。Java中的
    的头像 发表于 03-14 16:55 464次阅读

    AT socket可以多线程调用吗?

    请问AT socket 可以多线程调用吗? 有互锁机制吗,还是要自己做互锁。
    发表于 03-01 08:22

    redis多线程还能保证线程安全吗

    是单线程的,多个客户端请求会按序执行,每个请求使用一线程完成,这样可以避免多线程之间的竞争条件和锁等带来的开销。但是,由于Redis是存储内存中的数据的,当多个客户端同时对同一
    的头像 发表于 12-05 10:28 1407次阅读

    mfc多线程编程实例

    (图形用户界面)应用程序的开发。在这篇文章中,我们将重点介绍MFC中的多线程编程。 多线程编程在软件开发中非常重要,它可以实现程序的并发执行,提高程序的效率和响应速度。MFC提供了丰富的多线程支持,可以轻松地实现
    的头像 发表于 12-01 14:29 1206次阅读

    多线程如何保证数据的同步

    多线程编程是一种并发编程的方法,意味着程序中同时运行多个线程,每个线程可独立执行不同的任务,共享同一份数据。由于多线程并发执行的特点,会引发数据同步的问题,即保证多个
    的头像 发表于 11-17 14:22 951次阅读

    多线程并发查询oracle数据库

    数据库的原理、使用场景、实现方法以及可能遇到的问题和解决方案。 一、多线程并发查询的原理 在传统的单线程查询方式中,当一查询请求发起时,数据库会按照顺序执行查询语句并返回结果。如果查询语句比较复杂或者数据量比较大,查询的时
    的头像 发表于 11-17 14:22 3200次阅读

    多线程同步的几种方法

    ): 互斥锁是最基本的同步机制之一,它通过对临界区(一段代码或一段逻辑)加锁来保证同一时刻只能有一线程执行临界区的代码。当一
    的头像 发表于 11-17 14:16 1002次阅读

    Linux系统上多线程和多进程的运行效率

    深受其害。 经常在网络上看到有的XDJM问“多进程好还是多线程好?”、“Linux下用多进程还是多线程?”等等期望一劳永逸的问题,我只能说:没有最好,只有更好。根据实际情况来判断,哪个更加合适就是哪个好。 我们按照多个不同
    的头像 发表于 11-10 10:54 1133次阅读
    Linux系统上<b class='flag-5'>多线程</b>和多进程的运行效率

    关于Python多进程和多线程详解

    进程(process)和线程(thread)是操作系统的基本概念,但是它们比较抽象,不容易掌握。关于多进程和多线程,教科书上最经典的一句话是“进程是资源分配的最小单位,线程是CPU调度的最小单位”。
    的头像 发表于 11-06 14:46 722次阅读
    关于Python多进程和<b class='flag-5'>多线程</b>详解

    多线程idm下载软件

    多线程idm下载软件
    发表于 10-23 09:23 0次下载

    有许多线程的进程,如何才能改变其中一线程的工作目录?

    有许多线程的进程,我们如何才能改变其中一线程的工作目录,而不影响其他?
    发表于 10-17 09:01

    多线程的情况下如何对一值进行 a++ 操作

    多线程的情况下,对一值进行 a++ 操作,会出现什么问题? a++ 的问题 先写个 demo 的例子。把 a++ 放入多线程中运行一下。定义 10
    的头像 发表于 10-13 11:17 582次阅读
    在<b class='flag-5'>多线程</b>的情况下如何对一<b class='flag-5'>个</b>值进行 a++ 操作

    MDK可以支持多线程编译吗?

    怎么才能打开多线程编译
    发表于 10-11 07:23