2013年05月30日 Onetwogoo 评论 33 条评论 37,900 人阅读html
(本文由onetwogoo投稿)java
在《疫苗:Java HashMap的死循环》中,咱们看到,java.util.HashMap并不能直接应用于多线程环境。对于多线程环境中应用HashMap,主要有如下几种选择:git
而以上几种方法在实现的具体细节上,都或多或少地用到了互斥锁。互斥锁会形成线程阻塞,下降运行效率,并有可能产生死锁、优先级翻转等一系列问题。github
CAS(Compare And Swap)是一种底层硬件提供的功能,它能够将判断并更改一个值的操做原子化。关于CAS的一些应用,《无锁队列的实现》一文中有很详细的介绍。shell
在java.util.concurrent.atomic包中,Java为咱们提供了不少方便的原子类型,它们底层彻底基于CAS操做。数组
例如咱们但愿实现一个全局公用的计数器,那么能够:安全
1多线程 2性能 3atom 4 5 6 7 8 9 10 |
|
其中,compareAndSet方法会检查counter现有的值是否为oldValue,若是是,则将其设置为新值newValue,操做成功并返回true;不然操做失败并返回false。
当计算counter新值时,若其余线程将counter的值改变,compareAndSwap就会失败。此时咱们只需在外面加一层循环,不断尝试这个过程,那么最终必定会成功将counter值+1。(其实AtomicInteger已经为经常使用的+1/-1操做定义了incrementAndGet与decrementAndGet方法,之后咱们只需简单调用它便可)
除了AtomicInteger外,java.util.concurrent.atomic包还提供了AtomicReference和AtomicReferenceArray类型,它们分别表明原子性的引用和原子性的引用数组(引用的数组)。
在实现无锁HashMap以前,让咱们先来看一下比较简单的无锁链表的实现方法。
以插入操做为例:
但在操做中途,有可能其余线程在A与B直接也插入了一些节点(假设为D),若是咱们不作任何判断,可能形成其余线程插入节点的丢失。(见图3)咱们能够利用CAS操做,在为节点A的next指针赋值时,判断其是否仍然指向B,若是节点A的next指针发生了变化则重试整个插入操做。大体代码以下:
1 2 3 4 5 6 7 8 |
|
(Node类的next字段为AtomicReference<Node>类型,即指向Node类型的原子性引用)
无锁链表的查找操做与普通链表没有区别。而其删除操做,则须要找到待删除节点前方的节点A和后方的节点B,利用CAS操做验证并更新节点A的next指针,使其指向节点B。
HashMap主要有插入、删除、查找以及ReHash四种基本操做。一个典型的HashMap实现,会用到一个数组,数组的每项元素为一个节点的链表。对于此链表,咱们能够利用上文提到的操做方法,执行插入、删除以及查找操做,但对于ReHash操做则比较困难。
如图4,在ReHash过程当中,一个典型的操做是遍历旧表中的每一个节点,计算其在新表中的位置,而后将其移动至新表中。期间咱们须要操纵3次指针:
而这三次指针操做必须同时完成,才能保证移动操做的原子性。但咱们不难看出,CAS操做每次只能保证一个变量的值被原子性地验证并更新,没法知足同时验证并更新三个指针的需求。
因而咱们不妨换一个思路,既然移动节点的操做如此困难,咱们可使全部节点始终保持有序状态,从而避免了移动操做。在典型的HashMap实现中,数组的长度始终保持为2i,而从Hash值映射为数组下标的过程,只是简单地对数组长度执行取模运算(即仅保留Hash二进制的后i位)。当ReHash时,数组长度加倍变为2i+1,旧数组第j项链表中的每一个节点,要么移动到新数组中第j项,要么移动到新数组中第j+2i项,而它们的惟一区别在于Hash值第i+1位的不一样(第i+1位为0则仍为第j项,不然为第j+2i项)。
如图5,咱们将全部节点按照Hash值的翻转位序(如1101->1011)由小到大排列。当数组大小为8时,二、18在一个组内;三、十一、27在另外一个组内。每组的开始,插入一个哨兵节点,以方便后续操做。为了使哨兵节点正确排在组的最前方,咱们将正常节点Hash的最高位(翻转后变为最低位)置为1,而哨兵节点不设置这一位。
当数组扩容至16时(见图6),第二组分裂为一个只含3的组和一个含有十一、27的组,但节点之间的相对顺序并未改变。这样在ReHash时,咱们就不须要移动节点了。
因为扩容时数组的复制会占用大量的时间,这里咱们采用了将整个数组分块,懒惰创建的方法。这样,当访问到某下标时,仅需判断此下标所在块是否已创建完毕(若是没有则创建)。
另外定义size为当前已使用的下标范围,其初始值为2,数组扩容时仅需将size加倍便可;定义count表明目前HashMap中包含的总节点个数(不算哨兵节点)。
初始时,数组中除第0项外,全部项都为null。第0项指向一个仅有一个哨兵节点的链表,表明整条链的起点。初始时全貌见图7,其中浅绿色表明当前未使用的下标范围,虚线箭头表明逻辑上存在,但实际未创建的块。
初始化下标操做
数组中为null的项都认为处于未初始化状态,初始化某个下标即表明创建其对应的哨兵节点。初始化是递归进行的,即若其父下标未初始化,则先初始化其父下标。(一个下标的父下标是其移除最高二进制位后获得的下标)大体代码以下:
1 2 3 4 5 6 7 8 9 10 11 |
|
其中getBucket即封装过的获取数组某下标内容的方法,setBucket同理。listInsert将从指定位置开始查找适合插入的位置插入给定的节点,若链表中已存在hash相同的节点则返回那个已存在的节点;不然返回新插入的节点。
插入操做
查找操做
删除操做
《Split-Ordered Lists: Lock-Free Extensible Hash Tables》
(全文完)