<?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>并发 | 酷 壳 - CoolShell</title>
	<atom:link href="https://coolshell.cn/tag/%e5%b9%b6%e5%8f%91/feed" rel="self" type="application/rss+xml" />
	<link>https://coolshell.cn</link>
	<description>享受编程和技术所带来的快乐 - Coding Your Ambition</description>
	<lastBuildDate>Tue, 30 Jul 2013 01:49:49 +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>7个示例科普CPU Cache</title>
		<link>https://coolshell.cn/articles/10249.html</link>
					<comments>https://coolshell.cn/articles/10249.html#comments</comments>
		
		<dc:creator><![CDATA[Leo]]></dc:creator>
		<pubDate>Tue, 30 Jul 2013 01:05:38 +0000</pubDate>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[系统架构]]></category>
		<category><![CDATA[cache]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[并发]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=10249</guid>

					<description><![CDATA[<p>（感谢网友 @我的上铺叫路遥 翻译投稿） CPU cache一直是理解计算机体系架构的重要知识点，也是并发编程设计中的技术难点，而且相关参考资料如同过江之鲫，浩...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/10249.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/10249.html">7个示例科普CPU Cache</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>（感谢网友 </strong><a href="http://weibo.com/fullofbull" target="_blank"><strong>@我的上铺叫路遥</strong></a><strong> 翻译投稿）</strong></p>
<p>CPU cache一直是理解计算机体系架构的重要知识点，也是并发编程设计中的技术难点，而且相关参考资料如同过江之鲫，浩瀚繁星，阅之如临深渊，味同嚼蜡，三言两语难以入门。正好网上有人推荐了微软大牛Igor Ostrovsky一篇博文<strong>《漫游处理器缓存效应》</strong>，文章不仅仅用7个最简单的源码示例就将CPU cache的原理娓娓道来，还附加图表量化分析做数学上的佐证，个人感觉这种案例教学的切入方式绝对是俺的菜，故而忍不住贸然译之，以飨列位看官。</p>
<p>原文地址：<a href="http://igoro.com/archive/gallery-of-processor-cache-effects/">Gallery of Processor Cache Effects</a></p>
<p>大多数读者都知道cache是一种快速小型的内存，用以存储最近访问内存位置。这种描述合理而准确，但是更多地了解一些处理器缓存工作中的“烦人”细节对于理解程序运行性能有很大帮助。</p>
<p>在这篇博客中，我将运用代码示例来详解cache工作的方方面面，以及对现实世界中程序运行产生的影响。</p>
<p>下面的例子都是用C#写的，但语言的选择同程序运行状况以及得出的结论几乎没什么影响。</p>
<h4>示例1：内存访问和运行</h4>
<p>你认为相较于循环1，循环2会运行多快？</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">int[] arr = new int[64 * 1024 * 1024];

// Loop 1
for (int i = 0; i &lt; arr.Length; i++) arr[i] *= 3;

// Loop 2
for (int i = 0; i &lt; arr.Length; i += 16) arr[i] *= 3;</pre>
<p><span id="more-10249"></span></p>
<p>第一个循环将数组的每个值乘3，第二个循环将每16个值乘3，第二个循环只做了第一个约6%的工作，但在现代机器上，两者几乎运行相同时间：在我机器上分别是80毫秒和78毫秒。</p>
<p>两个循环花费相同时间的原因跟内存有关。<strong>循环执行时间长短由数组的内存访问次数决定的，而非整型数的乘法运算次数。</strong>经过下面对第二个示例的解释，你会发现硬件对这两个循环的主存访问次数是相同的。</p>
<h4>示例2：缓存行的影响</h4>
<p>让我们进一步探索这个例子。我们将尝试不同的循环步长，而不仅仅是1和16。</p>
<p><code data-enlighter-language="c" class="EnlighterJSRAW">for (int i = 0; i &lt; arr.Length; i += K) arr[i] *= 3;</code></p>
<p>下图为该循环在不同步长(K)下的运行时间：</p>
<p><img decoding="async" class="aligncenter" alt="running times of this loop for different step values (K)" src="http://igoro.com/wordpress/wp-content/uploads/2010/01/image6.png" /></p>
<p>注意当步长在1到16范围内，循环运行时间几乎不变。但从16开始，每次步长加倍，运行时间减半。</p>
<p>背后的原因是今天的CPU不再是按字节访问内存，而是以64字节为单位的块(chunk)拿取，称为一个缓存行(cache line)。当你读一个特定的内存地址，整个缓存行将从主存换入缓存，并且访问同一个缓存行内的其它值的开销是很小的。</p>
<p>由于16个整型数占用64字节（一个缓存行），for循环步长在1到16之间必定接触到相同数目的缓存行：即数组中所有的缓存行。当步长为32，我们只有大约每两个缓存行接触一次，当步长为64，只有每四个接触一次。</p>
<p>理解缓存行对某些类型的程序优化而言可能很重要。比如，数据字节对齐可能决定一次操作接触1个还是2个缓存行。那上面的例子来说，很显然操作不对齐的数据将损失一半性能。</p>
<h4>示例3：L1和L2缓存大小</h4>
<p>今天的计算机具有两级或三级缓存，通常叫做L1、L2以及可能的L3（译者注：如果你不明白什么叫二级缓存，可以参考<a href="https://coolshell.cn/articles/3236.html" target="_blank">这篇精悍的博文</a>lol）。如果你想知道不同缓存的大小，你可以使用系统内部工具<a href="http://technet.microsoft.com/en-us/sysinternals/cc835722.aspx" target="_blank">CoreInfo</a>，或者Windows API调用<a href="http://msdn.microsoft.com/en-us/library/ms683194(VS.85).aspx" target="_blank">GetLogicalProcessorInfo</a>。两者都将告诉你缓存行以及缓存本身的大小。</p>
<p>在我的机器上，CoreInfo现实我有一个32KB的L1数据缓存，一个32KB的L1指令缓存，还有一个4MB大小L2数据缓存。L1缓存是处理器独享的，L2缓存是成对处理器共享的。</p>
<p>Logical Processor to Cache Map:<br />
*&#8212; Data Cache 0, Level 1, 32 KB, Assoc 8, LineSize 64<br />
*&#8212; Instruction Cache 0, Level 1, 32 KB, Assoc 8, LineSize 64<br />
-*&#8211; Data Cache 1, Level 1, 32 KB, Assoc 8, LineSize 64<br />
-*&#8211; Instruction Cache 1, Level 1, 32 KB, Assoc 8, LineSize 64<br />
**&#8211; Unified Cache 0, Level 2, 4 MB, Assoc 16, LineSize 64<br />
&#8211;*- Data Cache 2, Level 1, 32 KB, Assoc 8, LineSize 64<br />
&#8211;*- Instruction Cache 2, Level 1, 32 KB, Assoc 8, LineSize 64<br />
&#8212;* Data Cache 3, Level 1, 32 KB, Assoc 8, LineSize 64<br />
&#8212;* Instruction Cache 3, Level 1, 32 KB, Assoc 8, LineSize 64<br />
&#8211;** Unified Cache 1, Level 2, 4 MB, Assoc 16, LineSize 64</p>
<p>（译者注：作者平台是四核机，所以L1编号为0~3，数据/指令各一个，L2只有数据缓存，两个处理器共享一个，编号0~1。关联性字段在后面例子说明。）</p>
<p>让我们通过一个实验来验证这些数字。遍历一个整型数组，每16个值自增1——一种节约地方式改变每个缓存行。当遍历到最后一个值，就重头开始。我们将使用不同的数组大小，可以看到当数组溢出一级缓存大小，程序运行的性能将急剧滑落。</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">int steps = 64 * 1024 * 1024;
// Arbitrary number of steps
int lengthMod = arr.Length - 1;
for (int i = 0; i &lt; steps; i++)
{
    arr[(i * 16) &amp; lengthMod]++; // (x &amp; lengthMod) is equal to (x % arr.Length)
}</pre>
<p>下图是运行时间图表：<br />
<img decoding="async" class="aligncenter" alt="cache size" src="http://igoro.com/wordpress/wp-content/uploads/2010/02/image.png" /></p>
<p>你可以看到在32KB和4MB之后性能明显滑落——正好是我机器上L1和L2缓存大小。</p>
<h4>示例4：指令级别并发</h4>
<p>现在让我们看一看不同的东西。下面两个循环中你以为哪个较快？</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">int steps = 256 * 1024 * 1024;
int[] a = new int[2];

// Loop 1
for (int i=0; i&lt;steps; i++) { a[0]++; a[0]++; }

// Loop 2
for (int i=0; i&lt;steps; i++) { a[0]++; a[1]++; }</pre>
<p>结果是第二个循环约比第一个快一倍，至少在我测试的机器上。为什么呢？这跟两个循环体内的操作指令依赖性有关。</p>
<p>第一个循环体内，操作做是相互依赖的（译者注：下一次依赖于前一次）：<br />
<img decoding="async" class="aligncenter" alt="same value dependency" src="http://igoro.com/wordpress/wp-content/uploads/2010/01/image.png" /><br />
但第二个例子中，依赖性就不同了：<br />
<img decoding="async" class="aligncenter" alt="different values dependency" src="http://igoro.com/wordpress/wp-content/uploads/2010/02/image2.png" /></p>
<p>现代处理器中对不同部分指令拥有一点并发性（译者注：跟流水线有关，比如Pentium处理器就有U/V两条流水线，后面说明）。这使得CPU在同一时刻访问L1两处内存位置，或者执行两次简单算术操作。在第一个循环中，处理器无法发掘这种指令级别的并发性，但第二个循环中就可以。</p>
<p>[原文更新]：许多人在reddit上询问有关编译器优化的问题，像{ a[0]++; a[0]++; }能否优化为{ a[0]+=2; }。实际上，C#编译器和CLR JIT没有做优化——在数组访问方面。我用release模式编译了所有测试（使用优化选项），但我查询了JIT汇编语言证实优化并未影响结果。</p>
<h4>示例5：缓存关联性</h4>
<p>缓存设计的一个关键决定是确保每个主存块(chunk)能够存储在任何一个缓存槽里，或者只是其中一些（译者注：此处一个槽位就是一个缓存行）。</p>
<p>有三种方式将缓存槽映射到主存块中：</p>
<ol>
<li><strong>直接映射(Direct mapped cache)</strong><br />
每个内存块只能映射到一个特定的缓存槽。一个简单的方案是通过块索引chunk_index映射到对应的槽位(chunk_index % cache_slots)。被映射到同一内存槽上的两个内存块是不能同时换入缓存的。（译者注：chunk_index可以通过物理地址/缓存行字节计算得到）</li>
<li><strong>N路组关联(N-way set associative cache)</strong><br />
每个内存块能够被映射到N路特定缓存槽中的任意一路。比如一个16路缓存，每个内存块能够被映射到16路不同的缓存槽。一般地，具有一定相同低bit位地址的内存块将共享16路缓存槽。（译者注：相同低位地址表明相距一定单元大小的连续内存）</li>
<li><strong>完全关联(Fully associative cache)</strong><br />
每个内存块能够被映射到任意一个缓存槽。操作效果上相当于一个散列表。</li>
</ol>
<p>直接映射缓存会引发冲突——当多个值竞争同一个缓存槽，它们将相互驱逐对方，导致命中率暴跌。另一方面，完全关联缓存过于复杂，并且硬件实现上昂贵。N路组关联是处理器缓存的典型方案，它在电路实现简化和高命中率之间取得了良好的折中。</p>
<p><img decoding="async" class="aligncenter" alt="完全关联与多路关联的cache映射" src="http://my.csdn.net/uploads/201204/18/1334757273_8141.png" /><br />
（此图由译者给出，直接映射和完全关联可以看做N路组关联的两个极端，从图中可知当N=1时，即直接映射；当N取最大值时，即完全关联。读者可以自行想象直接映射图例，具体表述见参考资料。）</p>
<p>举个例子，4MB大小的L2缓存在我机器上是16路关联。所有64字节内存块将分割为不同组，映射到同一组的内存块将竞争L2缓存里的16路槽位。</p>
<p>L2缓存有65,536个缓存行（译者注：4MB/64），每个组需要16路缓存行，我们将获得4096个集。这样一来，块属于哪个组取决于块索引的低12位bit(2^12=4096)。<strong>因此缓存行对应的物理地址凡是以262,144字节(4096*64)的倍数区分的，将竞争同一个缓存槽。我机器上最多维持16个这样的缓存槽。</strong>（译者注：请结合上图中的2路关联延伸理解，一个块索引对应64字节，chunk0对应组0中的任意一路槽位，chunk1对应组1中的任意一路槽位，以此类推chunk4095对应组4095中的任意一路槽位，chunk0和chunk4096地址的低12bit是相同的，所以chunk4096、chunk8192将同chunk0竞争组0中的槽位，它们之间的地址相差262,144字节的倍数，而最多可以进行16次竞争，否则就要驱逐一个chunk）。</p>
<p>为了使得缓存关联效果更加明了，我需要重复地访问同一组中的16个以上的元素，通过如下方法证明：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">public static long UpdateEveryKthByte(byte[] arr, int K)
{
    Stopwatch sw = Stopwatch.StartNew();
    const int rep = 1024*1024; // Number of iterations – arbitrary
    int p = 0;
    for (int i = 0; i &lt; rep; i++)
    {
        arr[p]++;
        p += K;
        if (p &gt;= arr.Length) p = 0;
    }
    sw.Stop();
    return sw.ElapsedMilliseconds;
}</pre>
<p>该方法每次在数组中迭代K个值，当到达末尾时从头开始。循环在运行足够长（2^20次）之后停止。</p>
<p>我使用不同的数组大小（每次增加1MB）和不同的步长传入UpdateEveryKthByte()。以下是绘制的图表，蓝色代表运行较长时间，白色代表较短时间：<br />
<img decoding="async" class="aligncenter" alt="timing" src="http://igoro.com/wordpress/wp-content/uploads/2010/02/image_thumb1_opt.png" /><br />
蓝色区域（较长时间）表明当我们重复数组迭代时，更新的值无法同时放在缓存中。浅蓝色区域对应80毫秒，白色区域对应10毫秒。</p>
<p>让我们来解释一下图表中蓝色部分：</p>
<p><strong>1.为何有垂直线？</strong>垂直线表明步长值过多接触到同一组中内存位置（大于16次）。在这些次数里，我的机器无法同时将接触过的值放到16路关联缓存中。</p>
<p>一些糟糕的步长值为2的幂：256和512。举个例子，考虑512步长遍历8MB数组，存在32个元素以相距262,144字节空间分布，所有32个元素都会在循环遍历中更新到，因为512能够整除262,144（译者注：此处一个步长代表一个字节）。</p>
<p>由于32大于16，这32个元素将一直竞争缓存里的16路槽位。</p>
<p>（译者注：为何512步长的垂直线比256步长颜色更深？在同样足够多的步数下，512比256访问到存在竞争的块索引次数多一倍。比如跨越262,144字节边界512需要512步，而256需要1024步。那么当步数为2^20时，512访问了2048次存在竞争的块而256只有1024次。最差情况下步长为262,144的倍数，因为每次循环都会引发一个缓存行驱逐。）</p>
<p>有些不是2的幂的步长运行时间长仅仅是运气不好，最终访问到的是同一组中不成比例的许多元素，这些步长值同样显示为蓝线。</p>
<p><strong>2.为何垂直线在4MB数组长度的地方停止？</strong>因为对于小于等于4MB的数组，16路关联缓存相当于完全关联缓存。</p>
<p>一个16路关联缓存最多能够维护16个以262,144字节分隔的缓存行，4MB内组17或更多的缓存行都没有对齐在262,144字节边界上，因为16*262,144=4,194,304。</p>
<p><strong>3.为何左上角出现蓝色三角？</strong>在三角区域内，我们无法在缓存中同时存放所有必要的数据，不是出于关联性，而仅仅是因为L2缓存大小所限。</p>
<p>举个例子，考虑步长128遍历16MB数组，数组中每128字节更新一次，这意味着我们一次接触两个64字节内存块。为了存储16MB数组中每两个缓存行，我们需要8MB大小缓存。但我的机器中只有4MB缓存（译者注：这意味着必然存在冲突从而延时）。</p>
<p>即使我机器中4MB缓存是全关联，仍无法同时存放8MB数据。</p>
<p><strong>4.为何三角最左边部分是褪色的？</strong>注意左边0~64字节部分——正好一个缓存行！就像上面示例1和2所说，额外访问相同缓存行的数据几乎没有开销。比如说，步长为16字节，它需要4步到达下一个缓存行，也就是说4次内存访问只有1次开销。</p>
<p>在相同循环次数下的所有测试用例中，采取省力步长的运行时间来得短。</p>
<p>将图表延伸后的模型：<br />
<img decoding="async" class="aligncenter" alt="timing2" src="http://igoro.com/wordpress/wp-content/uploads/2010/02/assoc_big_thumb1_opt.png" /></p>
<p>缓存关联性理解起来有趣而且确能被证实，但对于本文探讨的其它问题比起来，它肯定不会是你编程时所首先需要考虑的问题。</p>
<h4>示例6：缓存行的伪共享(false-sharing)</h4>
<p>在多核机器上，缓存遇到了另一个问题——一致性。不同的处理器拥有完全或部分分离的缓存。在我的机器上，L1缓存是分离的（这很普遍），而我有两对处理器，每一对共享一个L2缓存。这随着具体情况而不同，如果一个现代多核机器上拥有多级缓存，那么快速小型的缓存将被处理器独占。</p>
<p><strong>当一个处理器改变了属于它自己缓存中的一个值，其它处理器就再也无法使用它自己原来的值，因为其对应的内存位置将被刷新(invalidate)到所有缓存。而且由于缓存操作是以缓存行而不是字节为粒度，所有缓存中整个缓存行将被刷新！</strong></p>
<p>为证明这个问题，考虑如下例子：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">private static int[] s_counter = new int[1024];
private void UpdateCounter(int position)
{
    for (int j = 0; j &lt; 100000000; j++)
    {
        s_counter[position] = s_counter[position] + 3;
    }
}</pre>
<p>在我的四核机上，如果我通过四个线程传入参数0,1,2,3并调用UpdateCounter，所有线程将花费4.3秒。</p>
<p>另一方面，如果我传入16,32,48,64，整个操作进花费0.28秒！</p>
<p>为何会这样？第一个例子中的四个值很可能在同一个缓存行里，每次一个处理器增加计数，这四个计数所在的缓存行将被刷新，而其它处理器在下一次访问它们各自的计数（译者注：注意数组是private属性，每个线程独占）将失去命中(miss)一个缓存。这种多线程行为有效地禁止了缓存功能，削弱了程序性能。</p>
<h4>示例7：硬件复杂性</h4>
<p>即使你懂得了缓存的工作基础，有时候硬件行为仍会使你惊讶。不用处理器在工作时有不同的优化、探试和微妙的细节。</p>
<p>有些处理器上，L1缓存能够并发处理两路访问，如果访问是来自不同的存储体，而对同一存储体的访问只能串行处理。而且处理器聪明的优化策略也会使你感到惊讶，比如在伪共享的例子中，以前在一些没有微调的机器上运行表现并不良好，但我家里的机器能够对最简单的例子进行优化来减少缓存刷新。</p>
<p>下面是一个“硬件怪事”的奇怪例子：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">private static int A, B, C, D, E, F, G;
private static void Weirdness()
{
    for (int i = 0; i &lt; 200000000; i++)
    {
        // do something...
    }
}</pre>
<p>当我在循环体内进行三种不同操作，我得到如下运行时间：</p>
<p><strong>           操作</strong>                    <strong>时间</strong><br />
A++; B++; C++; D++;     719 ms<br />
A++; C++; E++; G++;     448 ms<br />
A++; C++;                      518 ms</p>
<p>增加A,B,C,D字段比增加A,C,E,G字段花费更长时间，更奇怪的是，增加A,C两个字段比增加A,C,E,G执行更久！</p>
<p>我无法肯定这些数字背后的原因，但我怀疑这跟存储体有关，如果有人能够解释这些数字，我将洗耳恭听。</p>
<p>这个例子的教训是，你很难完全预测硬件的行为。你可以预测很多事情，但最终，衡量及验证你的假设非常重要。</p>
<h4>关于第7个例子的一个回帖</h4>
<p>Goz：我询问Intel的工程师最后的例子，得到以下答复：</p>
<p>“很显然这涉及到执行单元里指令是怎样终止的，机器处理存储-命中-加载的速度，以及如何快速且优雅地处理试探性执行的循环展开（比如是否由于内部冲突而多次循环）。但这意味着你需要非常细致的流水线跟踪器和模拟器才能弄明白。在纸上预测流水线里的乱序指令是无比困难的工作，就算是设计芯片的人也一样。对于门外汉来说，没门，抱歉！”</p>
<h4>P.S.个人感悟——局部性原理和流水线并发</h4>
<p>程序的运行存在<strong>时间和空间上的局部性</strong>，前者是指只要内存中的值被换入缓存，今后一段时间内会被多次引用，后者是指该内存附近的值也被换入缓存。如果在编程中特别注意运用局部性原理，就会获得性能上的回报。</p>
<p>比如<strong>C语言中应该尽量减少静态变量的引用，</strong>这是因为静态变量存储在全局数据段，在一个被反复调用的函数体内，引用该变量需要对缓存多次换入换出，而如果是分配在堆栈上的局部变量，函数每次调用CPU只要从缓存中就能找到它了，因为堆栈的重复利用率高。</p>
<p>再比如<strong>循环体内的代码要尽量精简，</strong>因为代码是放在指令缓存里的，而指令缓存都是一级缓存，只有几K字节大小，如果对某段代码需要多次读取，而这段代码又跨越一个L1缓存大小，那么缓存优势将荡然无存。</p>
<p>关于<strong>CPU的流水线(pipeline)并发性</strong>简单说说，Intel Pentium处理器有两条流水线U和V，每条流水线可各自独立地读写缓存，所以可以在一个时钟周期内同时执行两条指令。但这两条流水线不是对等的，U流水线可以处理所有指令集，V流水线只能处理简单指令。</p>
<p>CPU指令通常被分为四类，第一类是常用的简单指令，像mov, nop, push, pop, add, sub, and, or, xor, inc, dec, cmp, lea，可以在任意一条流水线执行，只要相互之间不存在依赖性，完全可以做到指令并发。</p>
<p>第二类指令需要同别的流水线配合，像一些进位和移位操作，这类指令如果在U流水线中，那么别的指令可以在V流水线并发运行，如果在V流水线中，那么U流水线是暂停的。</p>
<p>第三类指令是一些跳转指令，如cmp,call以及条件分支，它们同第二类相反，当工作在V流水线时才能通U流水线协作，否则只能独占CPU。</p>
<p>第四类指令是其它复杂的指令，一般不常用，因为它们都只能独占CPU。</p>
<p>如果是汇编级别编程，<strong>要达到指令级别并发，必须要注重指令之间的配对。</strong>尽量使用第一类指令，避免第四类，还要在顺序上减少上下文依赖。</p>
<h4>参考资料</h4>
<p>wiki上的CPU cache解析（<a href="http://zh.wikipedia.org/zh-cn/CPU%E7%BC%93%E5%AD%98" target="_blank">中文版</a>）（<a href="https://en.wikipedia.org/wiki/CPU_cache" target="_blank">英文版</a>）。</p>
<p>上海交通大学师生制作的一个关于<a href="http://yoursunny.com/study/EI209/?topic=cache" target="_blank">cache映射功能、命中率计算</a>的教学演示程序，模拟了不同关联模式下cache的映射和命中几率，形象直观。</p>
<p>网易数据库大牛<a href="http://weibo.com/u/2216172320" target="_blank">@何_登成</a>自制PPT<a href="http://vdisk.weibo.com/s/dBzv2sibdUB8" target="_blank">《CPU Cache and Memory Ordering》</a>，信息量超大！</p>
<p>南京大学计算机教学<a href="http://cs.nju.edu.cn/swang/CompArchOrg_12F/slides/lecture09.pdf" target="_blank">公开PPT</a>，温馨提示，地址域名里面改变字段&#8221;lecture&#8221;后面的数字编号可切换课程;-)</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/20793.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/03/cpu_512x512-150x150.png" alt="与程序员相关的CPU缓存知识" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20793.html" class="wp_rp_title">与程序员相关的CPU缓存知识</a></li><li ><a href="https://coolshell.cn/articles/17416.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2016/07/cache-150x150.png" alt="缓存更新的套路" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17416.html" class="wp_rp_title">缓存更新的套路</a></li><li ><a href="https://coolshell.cn/articles/9703.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/05/图1-3-150x150.jpg" alt="无锁HashMap的原理与实现" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9703.html" class="wp_rp_title">无锁HashMap的原理与实现</a></li><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/2039.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/10.jpg" alt="CPU的性价比" width="150" height="150" /></a><a href="https://coolshell.cn/articles/2039.html" class="wp_rp_title">CPU的性价比</a></li><li ><a href="https://coolshell.cn/articles/3778.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/30.jpg" alt="敏捷水管工" width="150" height="150" /></a><a href="https://coolshell.cn/articles/3778.html" class="wp_rp_title">敏捷水管工</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/10249.html">7个示例科普CPU Cache</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/10249.html/feed</wfw:commentRss>
			<slash:comments>73</slash:comments>
		
		
			</item>
		<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" ><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>
		<item>
		<title>疫苗：Java HashMap的死循环</title>
		<link>https://coolshell.cn/articles/9606.html</link>
					<comments>https://coolshell.cn/articles/9606.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Fri, 10 May 2013 00:12:12 +0000</pubDate>
				<category><![CDATA[Java语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[Hash]]></category>
		<category><![CDATA[HashMap]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[多线程]]></category>
		<category><![CDATA[并发]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=9606</guid>

					<description><![CDATA[<p>在淘宝内网里看到同事发了贴说了一个CPU被100%的线上故障，并且这个事发生了很多次，原因是在Java语言在并发情况下使用HashMap造成Race Condi...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/9606.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/9606.html">疫苗：Java 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><img decoding="async" loading="lazy" class="size-medium wp-image-9618 alignright" alt="" src="https://coolshell.cn/wp-content/uploads/2013/05/race_condition-300x190.jpg" width="300" height="190" srcset="https://coolshell.cn/wp-content/uploads/2013/05/race_condition-300x190.jpg 300w, https://coolshell.cn/wp-content/uploads/2013/05/race_condition-426x270.jpg 426w, https://coolshell.cn/wp-content/uploads/2013/05/race_condition.jpg 549w" sizes="(max-width: 300px) 100vw, 300px" />在淘宝内网里看到同事发了贴说了一个CPU被100%的线上故障，并且这个事发生了很多次，原因是在Java语言在并发情况下使用HashMap造成Race Condition，从而导致死循环。这个事情我4、5年前也经历过，本来觉得没什么好写的，因为Java的HashMap是非线程安全的，所以在并发下必然出现问题。但是，我发现近几年，很多人都经历过这个事（在网上查“HashMap Infinite Loop”可以看到很多人都在说这个事）所以，觉得这个是个普遍问题，需要写篇疫苗文章说一下这个事，并且给大家看看一个完美的“Race Condition”是怎么形成的。</p>
<h4>问题的症状</h4>
<p>从前我们的Java代码因为一些原因使用了HashMap这个东西，但是当时的程序是单线程的，一切都没有问题。后来，我们的程序性能有问题，所以需要变成多线程的，于是，变成多线程后到了线上，发现程序经常占了100%的CPU，查看堆栈，你会发现程序都Hang在了HashMap.get()这个方法上了，重启程序后问题消失。但是过段时间又会来。而且，这个问题在测试环境里可能很难重现。</p>
<p>我们简单的看一下我们自己的代码，我们就知道HashMap被多个线程操作。而Java的文档说HashMap是非线程安全的，应该用ConcurrentHashMap。</p>
<p>但是在这里我们可以来研究一下原因。</p>
<p><span id="more-9606"></span></p>
<h4>Hash表数据结构</h4>
<p>我需要简单地说一下HashMap这个经典的数据结构。</p>
<p>HashMap通常会用一个指针数组（假设为table[]）来做分散所有的key，当一个key被加入时，会通过Hash算法通过key算出这个数组的下标i，然后就把这个&lt;key, value&gt;插到table[i]中，如果有两个不同的key被算在了同一个i，那么就叫冲突，又叫碰撞，这样会在table[i]上形成一个链表。</p>
<p>我们知道，如果table[]的尺寸很小，比如只有2个，如果要放进10个keys的话，那么碰撞非常频繁，于是一个O(1)的查找算法，就变成了链表遍历，性能变成了O(n)，这是Hash表的缺陷（可参看《<a title="Hash Collision DoS 问题" href="https://coolshell.cn/articles/6424.html" target="_blank" rel="bookmark">Hash Collision DoS 问题</a>》）。</p>
<p>所以，Hash表的尺寸和容量非常的重要。一般来说，Hash表这个容器当有数据要插入时，都会检查容量有没有超过设定的thredhold，如果超过，需要增大Hash表的尺寸，但是这样一来，整个Hash表里的无素都需要被重算一遍。这叫rehash，这个成本相当的大。</p>
<p>相信大家对这个基础知识已经很熟悉了。</p>
<h4>HashMap的rehash源代码</h4>
<p>下面，我们来看一下Java的HashMap的源代码。</p>
<p>Put一个Key,Value对到Hash表中：</p>
<pre data-enlighter-language="java" class="EnlighterJSRAW" data-enlighter-highlight="19">public V put(K key, V value)
{
    ......
    //算Hash值
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    //如果该key已被插入，则替换掉旧的value （链接操作）
    for (Entry&lt;K,V&gt; e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash &amp;&amp; ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    //该key不存在，需要增加一个结点
    addEntry(hash, key, value, i);
    return null;
}</pre>
<p>检查容量是否超标</p>
<pre data-enlighter-language="java" class="EnlighterJSRAW" data-enlighter-highlight="7">void addEntry(int hash, K key, V value, int bucketIndex)
{
    Entry&lt;K,V&gt; e = table[bucketIndex];
    table[bucketIndex] = new Entry&lt;K,V&gt;(hash, key, value, e);
    //查看当前的size是否超过了我们设定的阈值threshold，如果超过，需要resize
    if (size++ &gt;= threshold)
        resize(2 * table.length);
} </pre>
<p>新建一个更大尺寸的hash表，然后把数据从老的Hash表中迁移到新的Hash表中。</p>
<pre data-enlighter-language="java" class="EnlighterJSRAW" data-enlighter-highlight="9">void resize(int newCapacity)
{
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    ......
    //创建一个新的Hash Table
    Entry[] newTable = new Entry[newCapacity];
    //将Old Hash Table上的数据迁移到New Hash Table上
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}</pre>
<p>迁移的源代码，注意高亮处：</p>
<pre data-enlighter-language="java" class="EnlighterJSRAW" data-enlighter-highlight="12,14,15,16">void transfer(Entry[] newTable)
{
    Entry[] src = table;
    int newCapacity = newTable.length;
    //下面这段代码的意思是：
    //  从OldTable里摘一个元素出来，然后放到NewTable中
    for (int j = 0; j &lt; src.length; j++) {
        Entry&lt;K,V&gt; e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry&lt;K,V&gt; next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
} </pre>
<p>好了，这个代码算是比较正常的。而且没有什么问题。</p>
<h4>正常的ReHash的过程</h4>
<p>画了个图做了个演示。</p>
<ul>
<li>我假设了我们的hash算法就是简单的用key mod 一下表的大小（也就是数组的长度）。</li>
</ul>
<ul>
<li>最上面的是old hash 表，其中的Hash表的size=2, 所以key = 3, 7, 5，在mod 2以后都冲突在table[1]这里了。</li>
</ul>
<ul>
<li>接下来的三个步骤是Hash表 resize成4，然后所有的&lt;key,value&gt; 重新rehash的过程</li>
</ul>
<p style="text-align: center;"><img decoding="async" class="aligncenter  wp-image-9607" alt="" src="https://coolshell.cn/wp-content/uploads/2013/05/HashMap01.jpg" /></p>
<h4>并发下的Rehash</h4>
<p><strong>1）假设我们有两个线程。</strong>我用红色和浅蓝色标注了一下。</p>
<p>我们再回头看一下我们的 transfer代码中的这个细节：</p>
<pre data-enlighter-language="java" class="EnlighterJSRAW" data-enlighter-highlight="2">do {
    Entry&lt;K,V&gt; next = e.next; // &lt;--假设线程一执行到这里就被调度挂起了
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
} while (e != null);</pre>
<p>而我们的线程二执行完成了。于是我们有下面的这个样子。</p>
<p><img decoding="async" loading="lazy" class="aligncenter" alt="" src="https://coolshell.cn/wp-content/uploads/2013/05/HashMap02.jpg" width="616" height="434" /></p>
<p>注意，<strong>因为Thread1的 e 指向了key(3)，而next指向了key(7)，其在线程二rehash后，指向了线程二重组后的链表</strong>。我们可以看到链表的顺序被反转后。</p>
<p><strong>2）线程一被调度回来执行。</strong></p>
<ul>
<li><strong>先是执行 newTalbe[i] = e;</strong></li>
<li><strong>然后是e = next，导致了e指向了key(7)，</strong></li>
<li><strong>而下一次循环的next = e.next导致了next指向了key(3)</strong></li>
</ul>
<p><img decoding="async" loading="lazy" class="aligncenter" alt="" src="https://coolshell.cn/wp-content/uploads/2013/05/HashMap03.jpg" width="591" height="376" /></p>
<p><strong>3）一切安好。</strong></p>
<p>线程一接着工作。<strong>把key(7)摘下来，放到newTable[i]的第一个，然后把e和next往下移</strong>。</p>
<p><img decoding="async" loading="lazy" class="aligncenter" alt="" src="https://coolshell.cn/wp-content/uploads/2013/05/HashMap04.jpg" width="627" height="411" /></p>
<p><strong>4）环形链接出现。</strong></p>
<p><strong>e.next = newTable[i] 导致  key(3).next 指向了 key(7)</strong></p>
<p><strong>注意：此时的key(7).next 已经指向了key(3)， 环形链表就这样出现了。</strong></p>
<p style="text-align: left;"><img decoding="async" loading="lazy" class="aligncenter" alt="" src="https://coolshell.cn/wp-content/uploads/2013/05/HashMap05.jpg" width="623" height="395" /></p>
<p style="text-align: left;"><strong>于是，当我们的线程一调用到，HashTable.get(11)时，悲剧就出现了——Infinite Loop。</strong></p>
<h4 style="text-align: left;">其它</h4>
<p>有人把这个问题报给了Sun，不过Sun不认为这个是一个问题。因为HashMap本来就不支持并发。要并发就用ConcurrentHashmap</p>
<p><a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6423457" target="_blank">http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6423457</a></p>
<p>我在这里把这个事情记录下来，只是为了让大家了解并体会一下并发环境下的危险。</p>
<p>参考：<a href="http://mailinator.blogspot.com/2009/06/beautiful-race-condition.html" rel="nofollow">http://mailinator.blogspot.com/2009/06/beautiful-race-condition.html</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" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/9703.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/05/图1-3-150x150.jpg" alt="无锁HashMap的原理与实现" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9703.html" class="wp_rp_title">无锁HashMap的原理与实现</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/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><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/11541.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/24.jpg" alt="面向GC的Java编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11541.html" class="wp_rp_title">面向GC的Java编程</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></ul></div></div>The post <a href="https://coolshell.cn/articles/9606.html">疫苗：Java HashMap的死循环</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/9606.html/feed</wfw:commentRss>
			<slash:comments>181</slash:comments>
		
		
			</item>
	</channel>
</rss>
