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

总结: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就可以实现它光的宣称了。

获取连接

连接的获取是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方法。

归还连接

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

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住大部分的并发编程场景呢?主要还是这里面的知识点太多。下面我简单罗列一下,你可以逐个攻破。

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

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

作者简介:小姐姐味道  (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。

文章来源网络,作者:运维,如若转载,请注明出处:https://shuyeidc.com/wp/232355.html<

(0)
运维的头像运维
上一篇2025-04-20 03:18
下一篇 2025-04-20 03:19

相关推荐

  • 个人主题怎么制作?

    制作个人主题是一个将个人风格、兴趣或专业领域转化为视觉化或结构化内容的过程,无论是用于个人博客、作品集、社交媒体账号还是品牌形象,核心都是围绕“个人特色”展开,以下从定位、内容规划、视觉设计、技术实现四个维度,详细拆解制作个人主题的完整流程,明确主题定位:找到个人特色的核心主题定位是所有工作的起点,需要先回答……

    2025-11-20
    0
  • 社群营销管理关键是什么?

    社群营销的核心在于通过建立有温度、有价值、有归属感的社群,实现用户留存、转化和品牌传播,其管理需贯穿“目标定位-内容运营-用户互动-数据驱动-风险控制”全流程,以下从五个维度展开详细说明:明确社群定位与目标社群管理的首要任务是精准定位,需明确社群的核心价值(如行业交流、产品使用指导、兴趣分享等)、目标用户画像……

    2025-11-20
    0
  • 香港公司网站备案需要什么材料?

    香港公司进行网站备案是一个涉及多部门协调、流程相对严谨的过程,尤其需兼顾中国内地与香港两地的监管要求,由于香港公司注册地与中国内地不同,其网站若主要服务内地用户或使用内地服务器,需根据服务器位置、网站内容性质等,选择对应的备案路径(如工信部ICP备案或公安备案),以下从备案主体资格、流程步骤、材料准备、注意事项……

    2025-11-20
    0
  • 如何企业上云推广

    企业上云已成为数字化转型的核心战略,但推广过程中需结合行业特性、企业痛点与市场需求,构建系统性、多维度的推广体系,以下从市场定位、策略设计、执行落地及效果优化四个维度,详细拆解企业上云推广的实践路径,精准定位:明确目标企业与核心价值企业上云并非“一刀切”的方案,需先锁定目标客户群体,提炼差异化价值主张,客户分层……

    2025-11-20
    0
  • PS设计搜索框的实用技巧有哪些?

    在PS中设计一个美观且功能性的搜索框需要结合创意构思、视觉设计和用户体验考量,以下从设计思路、制作步骤、细节优化及交互预览等方面详细说明,帮助打造符合需求的搜索框,设计前的规划明确使用场景:根据网站或APP的整体风格确定搜索框的调性,例如极简风适合细线条和纯色,科技感适合渐变和发光效果,电商类则可能需要突出搜索……

    2025-11-20
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注