乐趣区

关于后端:读懂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 吧。当你把握了它,多线程的那些货色,不过是小菜一碟。

退出移动版