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