<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>onetwogoo | 酷 壳 - CoolShell</title>
	<atom:link href="https://coolshell.cn/articles/author/onetwogoo/feed" rel="self" type="application/rss+xml" />
	<link>https://coolshell.cn</link>
	<description>享受编程和技术所带来的快乐 - Coding Your Ambition</description>
	<lastBuildDate>Thu, 30 May 2013 13:37:10 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.2</generator>
	<item>
		<title>无锁HashMap的原理与实现</title>
		<link>https://coolshell.cn/articles/9703.html</link>
					<comments>https://coolshell.cn/articles/9703.html#comments</comments>
		
		<dc:creator><![CDATA[onetwogoo]]></dc:creator>
		<pubDate>Thu, 30 May 2013 13:31:20 +0000</pubDate>
				<category><![CDATA[Java语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[Hash]]></category>
		<category><![CDATA[HashMap]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[多线程]]></category>
		<category><![CDATA[并发]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=9703</guid>

					<description><![CDATA[<p> (本文由onetwogoo投稿) 在《疫苗：Java HashMap的死循环》中，我们看到，java.util.HashMap并不能直接应用于多线程环境。对于...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/9703.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/9703.html">无锁HashMap的原理与实现</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong> (本文由<a href="https://github.com/onetwogoo" rel="author">onetwogoo</a>投稿)</strong></p>
<p>在《<a title="疫苗：Java HashMap的死循环" href="https://coolshell.cn/articles/9606.html" target="_blank">疫苗：Java HashMap的死循环</a>》中，我们看到，java.util.HashMap并不能直接应用于多线程环境。对于多线程环境中应用HashMap，主要有以下几种选择：</p>
<ol>
<li><span style="line-height: 13px;">使用线程安全的java.util.Hashtable作为替代。</span></li>
<li>使用java.util.Collections.synchronizedMap方法，将已有的HashMap对象包装为线程安全的。</li>
<li>使用java.util.concurrent.ConcurrentHashMap类作为替代，它具有非常好的性能。</li>
</ol>
<p>而以上几种方法在实现的具体细节上，都或多或少地用到了互斥锁。互斥锁会造成线程阻塞，降低运行效率，并有可能产生死锁、优先级翻转等一系列问题。</p>
<p>CAS(Compare And Swap)是一种底层硬件提供的功能，它可以将判断并更改一个值的操作原子化。关于CAS的一些应用，《<a title="无锁队列的实现" href="https://coolshell.cn/articles/8239.html" target="_blank">无锁队列的实现</a>》一文中有很详细的介绍。</p>
<h4>Java中的原子操作</h4>
<p>在java.util.concurrent.atomic包中，Java为我们提供了很多方便的原子类型，它们底层完全基于CAS操作。</p>
<p>例如我们希望实现一个全局公用的计数器，那么可以：</p>
<p>&nbsp;</p>
<pre data-enlighter-language="java" class="EnlighterJSRAW">private AtomicInteger counter = new AtomicInteger(3);

public void addCounter() {
    for (;;) {
        int oldValue = counter.get();
        int newValue = oldValue + 1;
        if (counter.compareAndSet(oldValue, newValue))
            return;
    }
}</pre>
<p><span id="more-9703"></span></p>
<p>其中，compareAndSet方法会检查counter现有的值是否为oldValue，如果是，则将其设置为新值newValue，操作成功并返回true；否则操作失败并返回false。</p>
<p>当计算counter新值时，若其他线程将counter的值改变，compareAndSwap就会失败。此时我们只需在外面加一层循环，不断尝试这个过程，那么最终一定会成功将counter值+1。（其实AtomicInteger已经为常用的+1/-1操作定义了incrementAndGet与decrementAndGet方法，以后我们只需简单调用它即可）</p>
<p>除了AtomicInteger外，java.util.concurrent.atomic包还提供了AtomicReference和AtomicReferenceArray类型，它们分别代表原子性的引用和原子性的引用数组（引用的数组）。</p>
<h4>无锁链表的实现</h4>
<p>在实现无锁HashMap之前，让我们先来看一下比较简单的无锁链表的实现方法。</p>
<p>以插入操作为例：</p>
<ol>
<li><span style="line-height: 13px;">首先我们需要找到待插入位置前面的节点A和后面的节点B。</span></li>
<li><span style="line-height: 13px;">然后新建一个节点C，并使其next指针指向节点B。（见图1）</span></li>
<li>最后使节点A的next指针指向节点C。（见图2）</li>
</ol>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-9743" alt="" src="https://coolshell.cn/wp-content/uploads/2013/05/图1-3.jpg" width="600" height="479" srcset="https://coolshell.cn/wp-content/uploads/2013/05/图1-3.jpg 600w, https://coolshell.cn/wp-content/uploads/2013/05/图1-3-300x240.jpg 300w, https://coolshell.cn/wp-content/uploads/2013/05/图1-3-338x270.jpg 338w" sizes="(max-width: 600px) 100vw, 600px" /></p>
<p>但在操作中途，有可能其他线程在A与B直接也插入了一些节点（假设为D），如果我们不做任何判断，可能造成其他线程插入节点的丢失。（见图3）我们可以利用CAS操作，在为节点A的next指针赋值时，判断其是否仍然指向B，如果节点A的next指针发生了变化则重试整个插入操作。大致代码如下：</p>
<pre data-enlighter-language="java" class="EnlighterJSRAW">private void listInsert(Node head, Node c) {
    for (;;) {
        Node a = findInsertionPlace(head), b = a.next.get();
        c.next.set(b);
        if (a.next.compareAndSwap(b,c))
            return;
    }
}</pre>
<p>(Node类的next字段为AtomicReference&lt;Node&gt;类型，即指向Node类型的原子性引用)</p>
<p>无锁链表的查找操作与普通链表没有区别。而其删除操作，则需要找到待删除节点前方的节点A和后方的节点B，利用CAS操作验证并更新节点A的next指针，使其指向节点B。</p>
<h4>无锁HashMap的难点与突破</h4>
<p>HashMap主要有<strong>插入</strong>、<strong>删除</strong>、<strong>查找</strong>以及<strong>ReHash</strong>四种基本操作。一个典型的HashMap实现，会用到一个数组，数组的每项元素为一个节点的链表。对于此链表，我们可以利用上文提到的操作方法，执行插入、删除以及查找操作，但对于ReHash操作则比较困难。</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-9744" alt="" src="https://coolshell.cn/wp-content/uploads/2013/05/图4.jpg" width="648" height="265" srcset="https://coolshell.cn/wp-content/uploads/2013/05/图4.jpg 648w, https://coolshell.cn/wp-content/uploads/2013/05/图4-300x122.jpg 300w" sizes="(max-width: 648px) 100vw, 648px" /></p>
<p>如图4，在ReHash过程中，一个典型的操作是遍历旧表中的每个节点，计算其在新表中的位置，然后将其移动至新表中。期间我们需要操纵3次指针：</p>
<ol>
<li>将A的next指针指向D</li>
<li>将B的next指针指向C</li>
<li>将C的next指针指向E</li>
</ol>
<p>而这三次指针操作必须同时完成，才能保证移动操作的原子性。但我们不难看出，CAS操作每次只能保证<strong>一个</strong>变量的值被原子性地验证并更新，无法满足同时验证并更新三个指针的需求。</p>
<p>于是我们不妨换一个思路，既然移动节点的操作如此困难，我们可以使所有节点始终保持有序状态，从而避免了移动操作。在典型的HashMap实现中，数组的长度始终保持为2<sup>i</sup>，而从Hash值映射为数组下标的过程，只是简单地对数组长度执行取模运算（即仅保留Hash二进制的后i位）。当ReHash时，数组长度加倍变为2<sup>i+1</sup>，旧数组第j项链表中的每个节点，要么移动到新数组中第j项，要么移动到新数组中第j+2<sup>i</sup>项，而它们的唯一区别在于Hash值第i+1位的不同（第i+1位为0则仍为第j项，否则为第j+2<sup>i</sup>项）。</p>
<p style="text-align: center;"><img decoding="async" loading="lazy" class="aligncenter  wp-image-9745" alt="" src="https://coolshell.cn/wp-content/uploads/2013/05/图5-6.jpg" width="690" height="297" srcset="https://coolshell.cn/wp-content/uploads/2013/05/图5-6.jpg 863w, https://coolshell.cn/wp-content/uploads/2013/05/图5-6-300x128.jpg 300w" sizes="(max-width: 690px) 100vw, 690px" /></p>
<p>如图5，我们将所有节点按照Hash值的翻转位序（如1101-&gt;1011）由小到大排列。当数组大小为8时，2、18在一个组内；3、11、27在另一个组内。每组的开始，插入一个哨兵节点，以方便后续操作。为了使哨兵节点正确排在组的最前方，我们将正常节点Hash的最高位（翻转后变为最低位）置为1，而哨兵节点不设置这一位。</p>
<p>当数组扩容至16时（见图6），第二组分裂为一个只含3的组和一个含有11、27的组，但节点之间的相对顺序并未改变。这样在ReHash时，我们就不需要移动节点了。</p>
<h4>实现细节</h4>
<p>由于扩容时数组的复制会占用大量的时间，这里我们采用了将整个数组分块，懒惰建立的方法。这样，当访问到某下标时，仅需判断此下标所在块是否已建立完毕（如果没有则建立）。</p>
<p>另外定义size为当前已使用的下标范围，其初始值为2，数组扩容时仅需将size加倍即可；定义count代表目前HashMap中包含的总节点个数（不算哨兵节点）。</p>
<p>初始时，数组中除第0项外，所有项都为null。第0项指向一个仅有一个哨兵节点的链表，代表整条链的起点。初始时全貌见图7，其中浅绿色代表当前未使用的下标范围，虚线箭头代表逻辑上存在，但实际未建立的块。</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-9746" alt="" src="https://coolshell.cn/wp-content/uploads/2013/05/图7.jpg" width="446" height="282" srcset="https://coolshell.cn/wp-content/uploads/2013/05/图7.jpg 446w, https://coolshell.cn/wp-content/uploads/2013/05/图7-300x189.jpg 300w" sizes="(max-width: 446px) 100vw, 446px" /></p>
<h5>初始化下标操作</h5>
<p>数组中为null的项都认为处于未初始化状态，初始化某个下标即代表建立其对应的哨兵节点。初始化是递归进行的，即若其父下标未初始化，则先初始化其父下标。（一个下标的父下标是其移除最高二进制位后得到的下标）大致代码如下：</p>
<pre data-enlighter-language="java" class="EnlighterJSRAW">private void initializeBucket(int bucketIdx) {
    int parentIdx = bucketIdx ^ Integer.highestOneBit(bucketIdx);
    if (getBucket(parentIdx) == null)
        initializeBucket(parentIdx);

    Node dummy = new Node();
    dummy.hash = Integer.reverse(bucketIdx);
    dummy.next = new AtomicReference&amp;lt;&amp;gt;();

    setBucket(bucketIdx, listInsert(getBucket(parentIdx), dummy));
}</pre>
<p>其中getBucket即封装过的获取数组某下标内容的方法，setBucket同理。listInsert将从指定位置开始查找适合插入的位置插入给定的节点，若链表中已存在hash相同的节点则返回那个已存在的节点；否则返回新插入的节点。</p>
<h5>插入操作</h5>
<ul>
<li>首先用HashMap的size对键的hashCode取模，得到应插入的数组下标。</li>
<li>然后判断该下标处是否为null，如果为null则初始化此下标。</li>
<li>构造一个新的节点，并插入到适当位置，注意节点中的hash值应为原hashCode经过位翻转并将最低位置1之后的值。</li>
<li>将节点个数计数器加1，若加1后节点过多，则仅需将size改为size*2，代表对数组扩容（ReHash）。</li>
</ul>
<h5>查找操作</h5>
<ul>
<li>找出待查找节点在数组中的下标。</li>
<li>判断该下标处是否为null，如果为null则返回查找失败。</li>
<li>从相应位置进入链表，顺次寻找，直至找出待查找节点或超出本组节点范围。</li>
</ul>
<h5>删除操作</h5>
<ul>
<li><span style="line-height: 13px;">找出应删除节点在数组中的下标。</span></li>
<li>判断该下标处是否为null，如果为null则初始化此下标。</li>
<li>找到待删除节点，并从链表中删除。（注意由于哨兵节点的存在，任何正常元素只被其唯一的前驱节点所引用，不存在被前驱节点与数组中指针同时引用的情况，从而不会出现需要同时修改多个指针的情况）</li>
<li>将节点个数计数器减1。</li>
</ul>
<h4>参考文献</h4>
<p><a title="《Split-Ordered Lists: Lock-Free Extensible Hash Tables》" href="http://www.cs.ucf.edu/~dcm/Teaching/COT4810-Spring2011/Literature/SplitOrderedLists.pdf" target="_blank">《Split-Ordered Lists: Lock-Free Extensible Hash Tables》</a></p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" id="wp_rp_first"><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/9606.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/05/race_condition-150x150.jpg" alt="疫苗：Java HashMap的死循环" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9606.html" class="wp_rp_title">疫苗：Java HashMap的死循环</a></li><li ><a href="https://coolshell.cn/articles/11454.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/17.jpg" alt="从LongAdder看更高效的无锁实现" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11454.html" class="wp_rp_title">从LongAdder看更高效的无锁实现</a></li><li ><a href="https://coolshell.cn/articles/9169.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/02/Disruptor-150x150.png" alt="并发框架Disruptor译文" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9169.html" class="wp_rp_title">并发框架Disruptor译文</a></li><li ><a href="https://coolshell.cn/articles/6424.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/1.jpg" alt="Hash Collision DoS 问题" width="150" height="150" /></a><a href="https://coolshell.cn/articles/6424.html" class="wp_rp_title">Hash Collision DoS 问题</a></li><li ><a href="https://coolshell.cn/articles/22242.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/05/etcd-150x150.png" alt="ETCD的内存问题" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22242.html" class="wp_rp_title">ETCD的内存问题</a></li><li ><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/03/rust-social-wide-150x150.jpg" alt="Rust语言的编程范式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_title">Rust语言的编程范式</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/9703.html">无锁HashMap的原理与实现</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/9703.html/feed</wfw:commentRss>
			<slash:comments>35</slash:comments>
		
		
			</item>
	</channel>
</rss>
