ConcurrentHashMap核心原理,徹底給整明白了

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ConcurrentHashMap,它在技術面試中出現的頻率相當之高,所以我們必須對它深入理解和掌握。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"談到 ConcurrentHashMap,就一定會想到 HashMap。HashMap 在我們的代碼中使用頻率更高,不需要考慮線程安全的地方,我們一般都會使用 HashMap。HashMap 的實現非常經典,如果你讀過 HashMap 的源代碼,那麼對 ConcurrentHashMap 源代碼的理解會相對輕鬆,因爲兩者採用的數據結構是類似的"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這篇文章主要講解ConcurrentHashMap的核心原理,並註釋詳細源碼,文章篇幅較長,可收藏再看"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"基本結構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ConcurrentHashMap 是一個存儲 key/value 對的容器,並且是線程安全的。我們先看 ConcurrentHashMap 的存儲結構,如下圖:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ae/aef220bcfa46e904923f844205a74c2b.jpeg","alt":"ConcurrentHashMap核心原理,徹底給整明白了","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然 ConcurrentHashMap 的底層數據結構,和方法的實現細節和 HashMap 大體一致,但兩者在類結構上卻沒有任何關聯,我們看下 ConcurrentHashMap 的類圖:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/23/235b9cc134235a4902cf894c4129e113.jpeg","alt":"ConcurrentHashMap核心原理,徹底給整明白了","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看 ConcurrentHashMap 源碼,我們會發現很多方法和代碼和 HashMap 很相似,有的同學可能會問,爲什麼不繼承 HashMap 呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"繼承的確是個好辦法,但ConcurrentHashMap 都是在方法中間進行一些加鎖操作,也就是說加鎖把方法切割了,繼承就很難解決這個問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"ConcurrentHashMap和HashMap兩者的相同之處:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"數組、鏈表結構幾乎相同,所以底層對數據結構的操作思路是相同的(只是思路相同,底層實現不同);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"都實現了 Map 接口,繼承了 AbstractMap 抽象類,所以大多數的方法也都是相同的,HashMap 有的方法,ConcurrentHashMap 幾乎都有,所以當我們需要從 HashMap 切換到 ConcurrentHashMap 時,無需關心兩者之間的兼容問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"不同之處:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"紅黑樹結構略有不同,HashMap 的紅黑樹中的節點叫做 TreeNode,TreeNode 不僅僅有屬性,還維護着紅黑樹的結構,比如說查找,新增等等;ConcurrentHashMap 中紅黑樹被拆分成兩塊,TreeNode 僅僅維護的屬性和查找功能,新增了 TreeBin,來維護紅黑樹結構,並負責根節點的加鎖和解鎖;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"新增 ForwardingNode (轉移)節點,擴容的時候會使用到,通過使用該節點,來保證擴容時的線程安全。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這些概念名詞文章後面都會依次介紹"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"基本構成"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"重要屬性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們來看看 ConcurrentHashMap 的幾個重要屬性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"//這個Node數組就是ConcurrentHashMap用來存儲數據的哈希表。\ntransient volatile Node[] table\n//這是默認的初始化哈希表數組大小\nprivate static final int DEFAULT_CAPACITY = 16;\n//轉化爲紅黑樹的鏈表長度閾值\nstatic final int TREEIFY_THRESHOLD = 8\n//這個標識位用於識別擴容時正在轉移數據\nstatic final int MOVED = -1\n//計算哈希值時用到的參數,用來去除符號位\nstatic final int HASH_BITS = 0x7fffffff;\n//數據轉移時,新的哈希表數組\nprivate transient volatile Node[] nextTable;\n複製代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"重要組成元素"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Node"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鏈表中的元素爲Node對象。他是鏈表上的一個節點,內部存儲了key、value值,以及他的下一個節點的引用。這樣一系列的Node就串成一串,組成一個鏈表。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"”"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ForwardingNode"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當進行擴容時,要把鏈表遷移到新的哈希表,在做這個操作時,會在把數組中的頭節點替換爲ForwardingNode對象。ForwardingNode中不保存key和value,只保存了擴容後哈希表(nextTable)的引用。此時查找相應node時,需要去nextTable中查找。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"”"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TreeBin"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當鏈表轉爲紅黑樹後,數組中保存的引用爲 TreeBin,TreeBin 內部不保存 key/value,他保存了 TreeNode的list以及紅黑樹 root。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"”"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TreeNode"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"紅黑樹的節點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"”"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面依次講解各個核心方法,有詳細註釋"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"Put方法"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public V put(K key, V value) {\n return putVal(key, value, false);\n}\n複製代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7a/7a736d60e8af1e85e444c355193876c6.png","alt":"ConcurrentHashMap核心原理,徹底給整明白了","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ConcurrentHashMap 在 put 方法上的整體思路和 HashMap 相同,但在線程安全方面寫了很多保障的代碼,我們先來看下大體思路:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.如果數組爲空,初始化,初始化完成之後,走 2;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.計算當前槽點有沒有值,沒有值的話,cas 創建,失敗繼續自旋(for 死循環),直到成功,槽點有值的話,走 3;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3.如果槽點是轉移節點(正在擴容),就會一直自旋等待擴容完成之後再新增,不是轉移節點走 4;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4.槽點有值的,先鎖定當前槽點,保證其餘線程不能操作,如果是鏈表,新增值到鏈表的尾部,如果是紅黑樹,使用紅黑樹新增的方法新增;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5.新增完成之後 check 需不需要擴容,需要的話去擴容。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4e/4e121cc73f847396bedbb2fcc45ef51f.jpeg","alt":"ConcurrentHashMap核心原理,徹底給整明白了","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"ConcurrentHashMap在put過程中,採用了哪些手段來保證線程安全呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"數組初始化時的線程安全"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"數組初始化時,首先通過自旋來保證一定可以初始化成功,然後通過 CAS 設置 SIZECTL 變量的值,來保證同一時刻只能有一個線程對數組進行初始化,CAS 成功之後,還會再次判斷當前數組是否已經初始化完成,如果已經初始化完成,就不會再次初始化,通過自旋 + CAS + 雙重 check 等手段保證了數組初始化時的線程安全"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼接下來我們就來看看 initTable 方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/fa/fa4c5e4f2ba0286bfb281bcd6377e963.jpeg","alt":"ConcurrentHashMap核心原理,徹底給整明白了","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意裏面有個關鍵的值 sizeCtl,這個值有多個含義。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、-1 代表有線程正在創建 table;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、-N 代表有 N-1 個線程正在複製 table;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、在 table 被初始化前,代表根據構造函數傳入的值計算出的應被初始化的大小;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4、在 table 被初始化後,則被設置爲 table 大小 的 75%,代表 table 的容量(數組容量)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"新增槽點值時的線程安全"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此時爲了保證線程安全,做了四處優化:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.通過自旋死循環保證一定可以新增成功。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在新增之前,通過 for (Node[] tab = table;;)這樣的死循環來保證新增一定可以成功,一旦新增成功,就可以退出當前死循環,新增失敗的話,會重複新增的步驟,直到新增成功爲止。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.當前槽點爲空時,通過 CAS 新增。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Java 這裏的寫法非常嚴謹,沒有在判斷槽點爲空的情況下直接賦值,因爲在判斷槽點爲空和賦值的瞬間,很有可能槽點已經被其他線程複製了,所以我們採用 CAS 算法,能夠保證槽點爲空的情況下賦值成功,如果恰好槽點已經被其他線程賦值,當前 CAS 操作失敗,會再次執行 for 自旋,再走槽點有值的 put 流程,這裏就是自選 + CAS 的結合。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3.當前槽點有值,鎖住當前槽點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"put 時,如果當前槽點有值,就是 key 的 hash 衝突的情況,此時槽點上可能是鏈表或紅黑樹,我們通過鎖住槽點,來保證同一時刻只會有一個線程能對槽點進行修改"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"V oldVal = null;\n//鎖定當前槽點,其餘線程不能操作,保證了安全\nsynchronized (f) {\n複製代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4.紅黑樹旋轉時,鎖住紅黑樹的根節點,保證同一時刻,當前紅黑樹只能被一個線程旋轉"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"Hash算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"spread方法源碼分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"哈希算法的邏輯,決定 ConcurrentHashMap 保存和讀取速度。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"static final int spread(int h) {\n return (h ^ (h >>> 16)) & HASH_BITS;\n}\n複製代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"傳入的參數h爲 key 對象的 hashCode,spreed 方法對 hashCode 進行了加工。重新計算出 hash。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"hash 值是用來映射該 key 值在哈希表中的位置。取出哈希表中該 hash 值對應位置的代碼如下。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"tabAt(tab, i = (n - 1) & hash);\n複製代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們先看這一行代碼的邏輯,第一個參數爲哈希表,第二個參數是哈希表中的數組下標。通過 (n - 1) & hash 計算下標。n 爲數組長度,我們以默認大小 16 爲例,那麼 n-1 = 15,我們可以假設 hash 值爲 100"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"n的值15轉爲二進制:\n0000 0000 0000 0000 0000 0000 0000 1111\nhash的值100轉爲二進制:\n0000 0000 0000 0000 0000 0000 0110 0100。\n計算結果:\n0000 0000 0000 0000 0000 0000 0000 0100\n對應的十進制值爲 4\n複製代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"15的二進制高位都爲0,低位都是1。那麼經過&計算後,hash值100的高位全部被清零,低位則保持不變,並且一定是小於(n-1)的。也就是說經過如此計算,通過hash值得到的數組下標絕對不會越界。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"這裏提出幾個問題:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、數組大小可以爲 17,或者 18 嗎?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、如果爲了保證不越界爲什麼不直接用 % 計算取餘數?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、爲什麼不直接用 key 的 hashCode,而是使用經 spreed 方法加工後的 hash 值?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"數組大小必須爲 2 的 n 次方"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一個問題的答案是數組大小必須爲 2 的 n 次方,也就是 16、32、64….不能爲其他值。因爲如果不是 2 的 n 次方,那麼經過計算的數組下標會增大碰撞的機率"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果hash值的二進制是 10000(十進制16)、10010(十進制18)、10001(十進制17),和10100做&計算後,都是10000,也就是都被映射到數組16這個下標上。這三個值會以鏈表的形式存儲在數組16下標的位置。這顯然不是我們想要的結果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但如果數組長度n爲2的n次方,2進制的數值爲10,100,1000,10000……n-1後對應二進制爲1,11,111,1111……這樣和hash值低位&後,會保留原來hash值的低位數值,那麼只要hash值的低位不一樣,就不會發生碰撞。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時(n - 1) & hash等價於 hash%n。那麼爲什麼不直接用hash%n呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是因爲按位的操作效率會更高。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"爲什麼不直接用 key 的 hashCode?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實說到底還是爲了減少碰撞的概率。我們先看看 spreed 方法中的代碼做了什麼事情:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"h ^ (h >>> 16)\n複製代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個意思是把 h 的二進制數值向右移動 16 位。我們知道整形爲 32 位,那麼右移 16 位後,就是把高 16 位移到了低 16 位。而高 16 位清0了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"^爲異或操作,二進制按位比較,如果相同則爲 0,不同則爲 1。這行代碼的意思就是把高低16位做異或。如果兩個hashCode值的低16位相同,但是高位不同,經過如此計算,低16位會變得不一樣了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲什麼要把地位變得不一樣呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是由於哈希表數組長度n會是偏小的數值,那麼進行(n - 1) & hash運算時,一直使用的是hash較低位的值。那麼即使hash值不同,但如果低位相當,也會發生碰撞。而進行h ^ (h >>> 16)加工後的hash值,讓hashCode高位的值也參與了哈希運算,因此減少了碰撞的概率。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"(h ^ (h >>> 16)) & HASH_BITS\n複製代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲何高位移到低位和原來低位做異或操作後,還需要和HASH_BITS這個常量做 & 計算呢?HASH_BITS 這個常量的值爲 0x7fffffff,轉化爲二進制爲 0111 1111 1111 1111 1111 1111 1111 1111。這個操作後會把最高位轉爲 0,其實就是消除了符號位,得到的都是正數。這是因爲負的 hashCode 在ConcurrentHashMap 中有特殊的含義,因此我們需要得到一個正的 hashCode。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"擴容源碼分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們大致瞭解了ConcurrentHashMap 的存儲結構,那麼我們思考一個問題,當數組中保存的鏈表越來越多,那麼再存儲進來的元素大概率會插入到現有的鏈表中,而不是使用數組中剩下的空位。這樣會造成數組中保存的鏈表越來越長,由此導致哈希表查找速度下降,從 O(1) 慢慢趨近於鏈表的時間複雜度 O(n/2),這顯然違背了哈希表的初衷。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以ConcurrentHashMap 會做一個操作,稱爲擴容。也就是把數組長度變大,增加更多的空位出來,最終目的就是預防鏈表過長,這樣查找的時間複雜度纔會趨向於 O(1)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"擴容的操作並不會在數組沒有空位時才進行,因爲在空位快滿時,新保存元素更大的概率會命中已經使用的位置,那麼可能最後幾個桶位很難被使用,而鏈表卻越來越長了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外 ConcurrentHashMap 還會有鏈表轉紅黑樹的操作,以提高查找的速度,紅黑樹時間複雜度爲 O(logn),而鏈表是 O(n/2),因此只在 O(logn)= (MAXIMUM_CAPACITY >>> 1)) ?\n MAXIMUM_CAPACITY :\n tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));\n this.sizeCtl = cap;\n}\n複製代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是一個有參數的構造方法。如果你對未來存儲的數據量有預估,我們可以指定哈希表的大小,避免頻繁的擴容操作。tableSizeFor 這個方法確保了哈希表的大小永遠都是 2 的 n 次方。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意這裏傳入的參數不是 initialCapacity,而是 initialCapacity 的 1.5 倍 + 1。這樣做是爲了保證在默認 75% 的負載因子下,能夠足夠容納 initialCapacity 數量的元素。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"ConcurrentHashMap (int initialCapacity) 構造函數總結下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、構造函數中並不會初始化哈希表;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、構造函數中僅設置哈希表大小的變量 sizeCtl;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、initialCapacity 並不是哈希表大小;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4、哈希表大小爲 initialCapacity*1.5+1 後,向上取最小的 2 的 n 次方。如果超過最大容量一半,那麼就是最大容量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"tableSizeFor 是如何實現向上取得最接近入參 2 的 n 次方的。下面我們來看 tableSizeFor 源代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"private static final int tableSizeFor(int c) {\n int n = c - 1;\n n |= n >>> 1;\n n |= n >>> 2;\n n |= n >>> 4;\n n |= n >>> 8;\n n |= n >>> 16;\n return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;\n}\n複製代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"依舊是二進制按位操作,這樣一頓操作後,得到的數值就是大於 c 的最小 2 的 n 次。我們推演下過程,假設 c 是 9:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"1、int n = 9 - 1\nn=8\n2、n |= n >>> 1\nn=1000\nn >>> 1=0100\n兩個值按位或後\nn=1100\n3、n |= n >>> 2\nn=1100\nn >>> 2=0011\nn=1111\n複製代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到這裏可以看出規律來了。如果 c 足夠大,使得 n 很大,那麼運算到 n |= n >>> 16 時,n 的 32 位都爲 1。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總結一下這一段邏輯,其實就是把 n 有數值的 bit 位全部置爲 1。這樣就得到了一個肯定大於等於 n 的值。我們再看最後一行代碼,最終返回的是 n+1,那麼一個所有位都是 1 的二進制數字,+1 後得到的就是一個 2 的 n 次方數值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"看完三件事❤️"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"點贊,轉發,有你們的 『點贊和評論』,纔是我創造的動力。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"關注公衆號 『 "},{"type":"text","marks":[{"type":"strong"}],"text":"java爛豬皮"},{"type":"text","text":" 』,不定期分享原創知識。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"同時可以期待後續文章ing🚀"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/85/8518f1f13ab07e46122c84420bbf39a8.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章