【本文原创:银文杰】
不知道大家在面试时是否会被问到什么样的哈希数据结构可以保证线程安全?
很多小伙伴可能知道是ConcurrentHashMap,却对其没有太多了解,本文就带大家先来看一下ConcurrentHashMap集合中的size()方法是如何保证准确获取集合大小的。
我们可以思考一个最简单的场景,即调用者如何取得当前ConcurrentHashMap集合中的数据总量?
可能有一些读者会说,直接调用ConcurrentHashMap集合提供的size()方法即可;或者有一些读者会更进一步说,ConcurrentHashMap集合中提供了一个baseCount属性,每当完成添加操作时,ConcurrentHashMap集合都会在addCount方法中利用保证原子性的操作来更新baseCount属性的值。
是的,以上两种描述都没错,但是需要考虑一个前提,那就是ConcurrentHashMap集合工作在高并发场景下,可能随时有多个线程在进行读写操作。
在这样的前提下,再回头看以上两种描述可能就存在问题了。
首先,通过size()方法取得的当前集合中数据总量的值,很可能不是一个精确值,也就是在调用size()方法还未得到返回值时,集合中的数据总量可能就已经发生了变化。
然后,在addCount方法中确实可以利用保证原子性的操作来更新baseCount属性的值,但是若baseCount属性值更新失败了该怎么办?
最直观的处理思路是,一旦baseCount属性值更新失败,就进行重试。但很明显baseCount属性是一个被多线程共享操作的属性,如果采用典型的CAS设计思路——操作失败就重试,那么多线程操作下该值的更新大概率会失败且需要进行多次重试,从而形成并发场景下ConcurrentHashMap集合添加操作的明显性能瓶颈,如下图所示。
为了解决这个问题,最新版本的ConcurrentHashMap集合的主要设计思路是基于线程稳定不变的“探针”功能,设置多个不同的“计数槽”,保证大多数线程在更新计数值时不会产生原子操作冲突。
该部分的设计是ConcurrentHashMap集合中值得读者学习和在实际工作中借鉴的设计思路之一。
(1) ConcurrentHashMap集合中与数量计数有关的属性结构
ConcurrentHashMap集合中与数量计数有关的属性结构主要有3个,如下所示:
(2) CounterCell并发计数器的工作过程概要
ConcurrentHashMap集合的瞬时数据量等于baseCount与CounterCells数组中所有索引位上已记录的值之和。
我们可以看一下ConcurrentHashMap集合的size()方法的源代码:
如果ConcurrentHashMap集合工作在一个并发不高的环境中,ConcurrentHashMap集合会在新节点添加到集合中后,直接通过保证原子性的compareAndSetLong方法来完成baseCount计数器的数值增加工作;如果当前集合工作在并发较高的场景中(依据是通过compareAndSetLong方法来更新baseCount计数器时失败),就初始化counterCells数组,并在后续的处理过程中,在counterCells数组特定的索引位增加计数值。
这个过程就是addCount方法中第一个逻辑步骤所希望表达的:
这里有几个关键点需要进行说明。
ConcurrentHashMap集合使用counterCells数组而不是baseCount属性记录集合中的键值对数据量,前提条件就是通过compareAndSetLong方法进行baseCount属性的操作时,操作失败。
ConcurrentHashMap集合对counterCells数组进行计数增加和扩容操作的处理过程,被放置在fullAddCount方法中。后者也负责counterCells数组的初始化,它将counterCells数组的初始化长度设置为2。counterCells数组的每一个索引位只能通过保证原子性操作的compareAndSetLong方法来进行写处理。
特别注意addCount方法中使用的ThreadLocalRandom类,方法中使用了该工具类提供的getProbe()方法来获取当前线程的“探针”值。这是一个没有开放给最终程序员使用的功能,该功能将为调用者返回一个在当前线程中稳定不变的且全进程唯一的hash值。这个hash值可以在ThreadLocalRandom对象调用advanceProbe方法后发生变化。而当计算某个线程的计数值应该存放在counterCells数组的哪一个索引位时,使用的就是“探针”值和counterCells数组长度通过“与”运算取余数的方式完成的。
虽然counterCells数组的初始化长度只有2,也就是说瞬时只允许两个操作线程对counterCells数组中的不同索引位上的计数值进行成功修改,但该数组是可以被扩容的。每次扩容按照counterCells数组原始容量的2倍进行(即容量始终为2的倍数),且容量的最大上限为当前进程可使用的CPU内核个数(N)——超过当前进程可使用的CPU内核个数的counterCells数组大小是没有意义的,因为不会有那么多同时并发的线程数量。
counterCells数组扩容的条件很微妙,它并不是以某个既定的阈值为扩容标准的,而是“以当前进行ConcurrentHashMap集合操作的多个线程,在当前既定容量的counterCells数组的支持下,是否仍然存在抢占冲突的情况”来决定是否进行counterCells数组的扩容的。简单来说,就是执行compareAndSetLong(c, CELLVALUE, v = c.value, v + x)语句时返回了false的场景。
为保证所有操作线程都能了解到counterCells数组目前正在扩容,扩容时(还有counterCells数组初始化时或者counterCells数组某一个索引位初始化时)cellsBusy属性会被标记为1,下图可以描述counterCells数组工作的多个核心要点。
最后,counterCells数组某个索引位上的数据量计数值是由哪个线程执行增加操作的,实际上对于ConcurrentHashMap集合来说并不重要,重要的是保证数值可以正确记录。
也就是说,上一次Thread1完成数据添加后,可能在counterCells数组的0号索引位上进行计数值增加(+1),但是下一次Thread1完成数据添加后,又可能在counterCells数组的3号索引位上进行数值增加(+1)。
以上Thread线程变化所使用的counterCells数组索引位的情况确实是会发生的。
最典型的情况就是当前Thread线程使用原子操作更新某个索引位的计数值失败,但是又因为达到了counterCells数组的扩容上限而无法进行扩容,这时系统就会调用Thread线程对应的ThreadLocalRandom工具类的advanceProbe方法,以改变线程的“探针”值,最终达到更改其使用的counterCells数组索引位的目的。
本文摘自《Java高并发与集合框架:JCF和JUC源码分析与实现》一书!欢迎阅读此书了解更多相关内容!
《Java高并发与集合框架:JCF和JUC源码分析与实现》
银文杰 著
掌握Java集合框架和Java并发工具包,轻松应对80%的工作场景
本书并不讲解JCF和JUC中各个组件的基本使用方法,因为作者相信JCF和JUC中各种组件的基本使用方法大家通过查看网络资料就可以详细了解。
本书的主要内容是从源代码的层面剖析JCF和JUC的实现原理,以及讲解源代码中蕴含的理论知识,并讲解如何将这些大师级的理论知识应用到实际工作中。
因为是进行源代码分析,所以这本书包含了大量的Java原生模块的源代码,并且进行了逐行注释。请注意是逐行注释,所以在大家阅读本书时,不用担心无法读懂源代码。这解决了很多读者的源代码恐惧症问题。
这些被逐行注释、逐行剖析讲解的源代码,也是本书区别于市面上同类书籍的一大亮点。本书还包含了大量的工作原理图例,这些图例与每一个进行了源码剖析的知识点一一对应,形成了本书独具特点的图文并茂的讲解方式。
例如本书当然会详细剖析ConcurrentHashMap集合的结构和工作过程,但本书更关注ConcurrentHashMap集合为了适应高并发场景所做的典型化设计。
(扫码了解本书详情!)
机器学习火起来也有几年了, 当老姑大伯们渐渐把AI和程序员画上等号时,我大腿一拍大事不妙!生怕疫情后的家庭聚会上,让我表演才艺:做个什么狗陪他们下棋、做个什么精灵跟他们唠嗑…… 程序员群体很广的!我们也不是什么都懂,更何况我还...
隔离是指将系统或资源分割开,系统隔离是为了在系统发生故障时能限定传播范围和影响范围,即发生故障后不会出现滚雪球效应,从而保证只有出问题的服务不可用,其他服务还是可用的;而资源隔离有脏数据隔离、通过隔离后减少资源竞争提升性能等。我遇到的比...
了解智能一体化测试平台 智能一体化测试平台是为支持智能一体化测试理论而开发的平台,这个平台主要面向后台系统的服务/接口测试。借助这个平台,开发测试人员进行服务/接口测试时可以将工作重心集中在测试案例设计与管理上,测试执行与分析主要交...
读者评论