当前位置: 首页 > 后端技术 > Java

看完HikariCP一百行代码,多线程是孙子!

时间:2023-04-01 23:36:11 Java

总结:对于Java同学来说,能有机会通过看百行、十行代码来增加练习量是非常难得的。这是一个。通常,我看书的时候不会写代码,因为我的大脑被设置为单线程,一旦同时输入不同的信息,它就无法处理。但是多线程对于计算机来说是小菜一碟。它可以同时做很多事情,这看起来很不可思议。我真的希望将我的大脑皮层移植到这些很棒的设备上。用人脑去思考计算机正在思考的问题,本身就是一种折磨。但是在平时的工作和面试中,我们不得不面对这样的场景,所以多线程就成了编程路上的一颗难啃的骨头。HikariCP是SpringBoot默认的数据库连接池。它有一个名字,叫做不卑不亢的光,这让国内的德鲁伊很尴尬。言归正传,我们来看看Hikari中的ConcurrentBag。核心数据结构多线程代码是一个比较麻烦的问题,就是每一个API我都懂,就是不知道怎么用。很多熟悉concurrent包的同学,在面对实际问题的时候,还是被迫在最后加上Lock或者synchronized。ConcurrentBag是一个Lockfree数据结构,主要用于数据库连接的存储。可以说整个HikariCP的核心就是它。去掉乱七八糟的注释和异常处理,可以说关键代码只有一百多行,但是里面的方法很多。ConcurrentBag非常快。要实现这个目标,需要一定的核心数据结构支持。privatefinalCopyOnWriteArrayListsharedList;privatefinalThreadLocal>threadList;privatefinalAtomicIntegerwaiters;privatefinalSynchronousQueuehandoffQueue;copycodesharedList用于缓存所有连接,是一个CopyOnWriteArrayList结构。threadList用于缓存一个线程使用的所有连接,相当于一个快速引用,是一个ThreadLocal类型的ArrayList。waiters当前获取连接的服务员数量。AtomicInteger是一个自增对象。当等待者数大于0时,说明有线程正在获取资源。HandoffQueue0-capacity快速传递队列,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就可以实现它的轻量级要求了。获取连接获取连接是借用方法,也可以传入超时作为超时控制。publicTborrow(longtimeout,finalTimeUnittimeUnit)throwsInterruptedException首先,如果一个线程执行速度非常快,使用了很多连接,可以使用ThreadLocal快速获取连接对象,而不用跑到大池中去获取。代码如下所示。//首先尝试线程本地列表finalvarlist=threadList.get();for(inti=list.size()-1;i>=0;i--){finalvarentry=list.remove(i);最终TbagEntry=weakThreadLocals?((WeakReference)entry).get():(T)entry;if(bagEntry!=null&&bagEntry.compareAndSet(STATE_NOT_IN_USE,STATE_IN_USE)){returnbagEntry;}}Copy我们都知道代码,包括一些ArrayList和HashMap的基本结构是FailFast。如果在遍历的时候删除了一些数据,可能会出问题。幸运的是,由于我们的List是从ThreadLocal中获取的,所以它在第一时间避免了线程安全问题。接下来是遍历。这段代码使用了尾部遍历(头部遍历会出错),用于快速从链表中找到一个可重用的对象,然后使用CAS将状态投入使用。但如果该对象正在被使用,则直接将其删除。在ConcurrentBag中,每个ThreadLocal最多缓存50个连接对象引用。当在ThreadLocal中找不到可重用的对象时,就会去大池中去获取。也就是下面的代码。//否则,扫描共享列表...然后轮询切换队列我们可能偷了另一个服务员的连接,请求另一个袋子添加。如果(等待>1){listener.addBagItem(等待-1);}返回bagEntry;}}listener.addBagItem(等待);需要等待别人释放timeout=timeUnit.toNanos(timeout);do{finalvarstart=currentTime();finalTbagEntry=handoffQueue.poll(timeout,NANOSECONDS);如果(bagEntry==null||bagEntry.compareAndSet(STATE_NOT_IN_USE,STATE_IN_USE)){returnbagEntry;}timeout-=elapsedNanos(start);}while(timeout>10_000);返回空;}最后{waiters.decrementAndGet();执行的线程不同,所以必须考虑线程安全问题。由于shardList是线程安全的CopyOnWriteArrayList,适合读多写少的场景,所以我们直接遍历即可。这段代码的目的是一样的,需要从sharedList中找到一个空闲的连接对象。这里将自增等待变量传递给外部代码进行处理,主要是因为我们要根据等待的大小来判断是否创建新的对象。如果无法从池中获取连接,则需要等待其他线程释放一些资源。创建对象的过程是异步的,要获取它,需要依赖一段循环代码。while循环代码是纳秒精度的,会尝试从handoffQueue中获取它。最后会调用SynchronousQueue的transfer方法。返回连接被借用并返回。当连接用完时,它将返回到池中。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)){返回;}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住大部分的并发编程场景呢?主要是这里的知识点太多了。下面我简单罗列一下,大家可以一一攻破。使用ThreadLocal缓存本地资源引用,使用线程关闭资源减少锁冲突,使用读多写少线程安全的CopyOnWriteArrayList缓存所有对象,几乎不影响读取效率使用CAS-basedAtomicInteger计算个数的waiters,无锁操作使得计算速度更快。0容量交换队列SynchronousQueue使得对象传递更快。compareAndSet的CAS原语用于控制状态的变化,安全高效。很多核心代码都是这样设计的。在循环中使用park、yield等方法,避免无限循环占用大量CPU。您需要了解并发数据结构中的offer、poll、peek、put、take、add和remove方法之间的区别,并保持灵活性。volatile关键字用于在设置状态时修改应用程序CAS。volatile的使用也是一个常见的优化点。有必要了解WeakReference弱引用在垃圾回收时的表现。麻雀虽小,五脏俱全。如果您想将您的多线程编程技能提升到一个新的水平,请阅读这个简短而简洁的ConcurrentBag。当你掌握了它的窍门时,多线程的东西就是小菜一碟。