解析HikariCP一百行代码轻松掌握多线程
作者:小姐姐味道 发布时间:2023-03-21 19:11:00
引言
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吧。当你掌握了它,多线程的那些东西,不过是小菜一碟。
来源:https://juejin.cn/post/7145442200275386404
猜你喜欢
- 1、什么是过滤器?在客户端到服务器的过程中,当发送请求时,如果有不符合的信息将会被filter进行拦截,如果符合则会进行放行,在服务器给客户
- 本文简单介绍如何引入validation的步骤,如何通过自定义validation减少代码量,提高生产力。特别提及:非基本类型属性的vali
- start方法和run方法$start()$方法用来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到$cpu$时间片,就
- BigDecimal的舍入模式(RoundingMode)BigDecimal.divide方法中必须设置roundingMode,不然会报
- 背景今天学习Springboot,但是用的apache-maven 3.0 ,导入springboot1.5.19 ,Maven项目老是爆红
- java 读取网页内容的实例详解import java.io.BufferedReader; import java.io.IOExcept
- 1. 引入当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象, 只有通过new关键字才会产生出对象,这时系统才会
- 引言用过Spring Cloud的同学都知道在使用动态配置刷新的我们要配置一个 @RefreshScope,在类上才可以实现对象属性的的动态
- 目前市面上流行的爬虫以python居多,简单了解之后,觉得简单的一些页面的爬虫,主要就是去解析目标页面(html)。那么就在想,java有没
- 适配器(Adapter)模式:适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一
- 简介在实现登录功能时,一般为了安全都会设置验证码登录,为了防止某个用户用特定的程序暴力破解方式进行不断的尝试登录。常见验证码分为图片验证码和
- 1,获取当前线程信息Thread.CurrentThread 是一个 静态的 Thread 类,Thread 的CurrentTh
- 本文实例讲述了Java链表中元素删除的实现方法。分享给大家供大家参考,具体如下:该部分与上一节是息息相关的,关于如何在链表中删除元素,我们一
- 本文实例为大家分享了java实现滑动验证解锁的具体代码,供大家参考,具体内容如下1.html:<div class="dra
- 引言使用SpringMVC作为Controller层进行Web开发时,经常会需要对Controller中的方法进行参数检查。本来Spring
- 引导要求:线程资源必须通过线程池提供,不允许在应用自行显式创建线程;说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开
- 摘要:vs2019新鲜出炉,配置opencv又有哪些不一样呢,这个教程将会一步一步的教你如何配置opencv和跑动opencv一个简单的项目
- 本文实例讲述了Java Scanner类用法及nextLine()产生的换行符问题。分享给大家供大家参考,具体如下:分析理解:Scanner
- 用Linq从一个集合选取几列得到一个新的集合-可改列名
- 前言本篇内容:提示语的国际化返回,自定义多语言。本文使用aop方式,拦截接口返回的数据,进行转换。正文 先看这次示例教