ConcurrentHashMap overview

英文原文

Overview:

The primary design goal of this hash table is to maintain concurrent readability (typically method get(), but also iterators and related methods) while minimizing update contention. Secondary goals are to keep space consumption about the same or better than java.util.HashMap, and to support high initial insertion rates on an empty table by many threads.

此哈希表的主要設計目標是保持併發可讀性(通常爲方法 get(),但也支持迭代器和相關方法),同時最小化更新爭用。次要目標是保持空間消耗與 java.util.HashMap 相同或更好,並支持許多線程在空表上建立較高的初始插入速率。

This map usually acts as a binned (bucketed) hash table. Each key-value mapping is held in a Node. Most nodes are instances of the basic Node class with hash, key, value, and next fields. However, various subclasses exist: TreeNodes are arranged in balanced trees, not lists. TreeBins hold the roots of sets of TreeNodes. ForwardingNodes are placed at the heads of bins during resizing. ReservationNodes are used as placeholders while establishing values in computeIfAbsent and related methods. The types TreeBin, ForwardingNode, and ReservationNode do not hold normal user keys, values, or hashes, and are readily distinguishable during search etc because they have negative hash fields and null key and value fields. (These special nodes are either uncommon or transient, so the impact of carrying around some unused fields is insignificant.)

此 map 通常充當 binned(bucketed) 哈希表。 每個鍵值映射都位於節點(Node)中。 大多數節點是基本 Node 類的實例,具有哈希(hash)、鍵(key)、值(value)和下一個字段(next fields)。但是,存在各種子類:樹節點(TreeNodes)排列在平衡的樹中,而不是列表(list)中。 樹箱(TreeBins)保存樹節點集的根(the roots of sets of TreeNodes)。在調整大小期間,轉發節點(ForwardingNodes)放置在料箱(bins)的頭部。預留節點(ReservationNodes)用作佔位符(placeholders),同時在用computeIfAbsent和相關方法中建立值。 TreeBin、轉發節點(ForwardingNode)和保留節點(ReservationNode)的類型不保存正常的用戶鍵(key)、值(value)或哈希值(hashes),並且在搜索過程中很容易區分,因爲它們具有負哈希字段(negative hash fields)和空鍵(null key)和值(null value)字段。(這些特殊節點不常見或暫時性,因此攜帶一些未使用的字段的影響是微不足道的。

The table is lazily initialized to a power-of-two size upon the first insertion. Each bin in the table normally contains a list of Nodes (most often, the list has only zero or one Node). Table accesses require volatile/atomic reads, writes, and CASes. Because there is no other way to arrange this without adding further indirections, we use intrinsics (jdk.internal.misc.Unsafe) operations.

第一次插入時,該表被懶初始化(lazily initialized)爲二的次方大小(power-of-two size)。 表中的每個 bin 通常包含一個節點列表(a list of Nodes)(通常情況下,該列表只有零個節點或一個節點)。 表訪問需要可變/原子讀取、寫入和 CAS。(Table accesses require volatile/atomic reads, writes, and CASes. compare and set) 由於沒有其他方法可以安排,而不進一步添加雙向(indirections),我們使用內部函數(jdk.internal.misc.Unsafe)操作。

We use the top (sign) bit of Node hash fields for control purposes – it is available anyway because of addressing constraints. Nodes with negative hash fields are specially handled or ignored in map methods.

我們使用 Node 哈希字段的頂部(sign)位進行控制 - 由於尋址限制,它無論如何都可用。 具有負哈希字段的節點在映射方法中特別處理或忽略。

Insertion (via put or its variants) of the first node in an empty bin is performed by just CASing it to the bin. This is by far the most common case for put operations under most key/hash distributions. Other update operations (insert, delete, and replace) require locks. We do not want to waste the space required to associate a distinct lock object with each bin, so instead use the first node of a bin list itself as a lock. Locking support for these locks relies on builtin “synchronized” monitors.

將第一個節點插入(通過放置或其變體)到空 bin 中,只需將其緩存到 bin 中,就執行。 到目前爲止,這是在大多數鍵/哈希(key/hash)分佈下放置操作的最常見情況。 其他更新操作(插入、刪除和替換)需要鎖。 我們不想浪費將不同的鎖對象與每個 bin 相關聯所需的空間,而是使用 bin 列表本身的第一個節點作爲鎖。鎖定這些鎖的支持依賴於內置的"同步(synchronized)"監視器monitors。

Using the first node of a list as a lock does not by itself suffice though: When a node is locked, any update must first validate that it is still the first node after locking it, and retry if not. Because new nodes are always appended to lists, once a node is first in a bin, it remains first until deleted or the bin becomes invalidated (upon resizing).

但是,將列表的第一個節點用作鎖本身是不夠的:當節點被鎖定時,任何更新都必須首先驗證它仍然是鎖定後的第一個節點,如果沒有,則重試(retry)。由於新節點始終追加到列表中,因此一旦節點首先位於 bin 中,它將保持第一,直到刪除或 bin 失效(調整大小時)。

The main disadvantage of per-bin locks is that other update operations on other nodes in a bin list protected by the same lock can stall, for example when user equals() or mapping functions take a long time. However, statistically, under random hash codes, this is not a common problem. Ideally, the frequency of nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on average, given the resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-0.5) pow(0.5, k) / factorial(k)). The first values are:

每個 bin 鎖(per-bin locks)的主要缺點是,受同一鎖保護的 bin 列表中其他節點上的其他更新操作可能會停止,例如當用戶equals() 或 映射函數(mapping functions)需要很長時間時。 但是,在統計上,在隨機哈希代碼下,這不是一個常見的問題。 理想情況下,bin 中的節點頻率遵循 泊松分佈 (http://en.wikipedia.org/wiki/Poisson_distribution),參數平均約爲 0.5,因爲調整大小閾值爲 0.75,儘管由於調整大小,其差異較大粒 度。忽略方差,列表大小 k 的預期出現數爲 (exp(-0.5) pow(0.5,k) / 因子(k)。)。第一個值是:

  0:    0.60653066
  1:    0.30326533
  2:    0.07581633
  3:    0.01263606
  4:    0.00157952
  5:    0.00015795
  6:    0.00001316
  7:    0.00000094
  8:    0.00000006
  more: less than 1 in ten million

Lock contention probability for two threads accessing distinct elements is roughly 1 / (8 #elements) under random hashes.
在隨機哈希下,訪問不同元素的兩個線程的鎖爭用概率約爲 1/(8 #elements)。

Actual hash code distributions encountered in practice sometimes deviate significantly from uniform randomness. This includes the case when N > (1<<30), so some keys MUST collide. Similarly for dumb or hostile usages in which multiple keys are designed to have identical hash codes or ones that differs only in masked-out high bits. So we use a secondary strategy that applies when the number of nodes in a bin exceeds a threshold. These TreeBins use a balanced tree to hold nodes (a specialized form of red-black trees), bounding search time to O(log N). Each search step in a TreeBin is at least twice as slow as in a regular list, but given that N cannot exceed (1<<64) (before running out of addresses) this bounds search steps, lock hold times, etc, to reasonable constants (roughly 100 nodes inspected per operation worst case) so long as keys are Comparable (which is very common – String, Long, etc). TreeBin nodes (TreeNodes) also maintain the same “next” traversal pointers as regular nodes, so can be traversed in iterators in the same way. The table is resized when occupancy exceeds a percentage threshold (nominally, 0.75, but see below). Any thread noticing an overfull bin may assist in resizing after the initiating thread allocates and sets up the replacement array.

實際遇到的哈希編碼分佈有時明顯偏離統一隨機性。 這包括 N >(1<<30) 時的情況,因此某些鍵必須碰撞。 同樣,對於啞巴(dumb)或惡意(hostile)用法,其中多個鍵(multiple keys)設計爲具有相同的哈希代碼或僅在屏蔽高位不同的哈希代碼。因此,我們使用輔助策略,當 bin 中的節點數超過閾值時應用。這些樹箱(TreeBins)使用平衡樹(balanced tree)來保存節點(紅黑樹的一種特殊形式),將搜索時間限制爲 O(log N)。 TreeBin 中的每個搜索步驟的速度至少是常規列表的兩倍,但考慮到 N 不能超過 (1<<64) (在地址用完之前),此邊界搜索步驟、鎖定保留時間等,以合理常量(最差的情況下每個操作檢查大約 100 個節點),只要鍵是可比較的(這是很常見的 – String, Long, etc)。 TreeBin 的節點(TreeNodes)也保持與常規節點相同的"下一個"遍歷指針(“next” traversal pointers),因此可以以同樣的方式在迭代器中遍歷。 當佔用率超過百分比閾值時,將調整表的大小(通常是爲 0.75,但見下文)。 任何注意到過滿箱的(overfull bin)線程都可能有助於在啓動線程分配和設置替換數組後調整大小。

However, rather than stalling, these other threads may proceed with insertions etc. The use of TreeBins shields us from the worst case effects of overfilling while resizes are in progress. Resizing proceeds by transferring bins, one by one, from the table to the next table. However, threads claim small blocks of indices to transfer (via field transferIndex) before doing so, reducing contention. A generation stamp in field sizeCtl ensures that resizings do not overlap. Because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset. We eliminate unnecessary node creation by catching cases where old nodes can be reused because their next fields won’t change. On average, only about one-sixth of them need cloning when a table doubles. The nodes they replace will be garbage collectible as soon as they are no longer referenced by any reader thread that may be in the midst of concurrently traversing table. Upon transfer, the old table bin contains only a special forwarding node (with hash field “MOVED”) that contains the next table as its key. On encountering a forwarding node, access and update operations restart, using the new table.

但是,這些其他線程可能會繼續進行插入等,而不是停滯。 使用 TreeBins 保護我們免受過度填充的最壞情況影響,而調整大小正在進行中。 通過將 bins 逐個從錶轉移到下一個表來調整大小。但是,線程在這樣做之前要求傳輸小塊索引(small blocks of indices)(通過字段 transferIndex),從而減少爭用。 字段 sizeCtl 中的生成stamp 可確保調整大小不會疊加覆蓋。由於我們使用的是二次擴展,因此每個 bin 中的元素必須保持相同的索引,或者以 2 的 次方數 移動。我們通過捕獲舊節點可以重複使用的情況來消除不必要的節點創建,因爲舊節點的下一個字段不會更改。 平均而言,當表翻倍時,只有大約六分之一需要克隆。它們替換的節點將可進行垃圾回收,只要它們不再被可能處於併發遍表中間的任何讀取器線程引用。 傳輸時,舊錶箱僅包含一個特殊的轉發節點(哈希字段"MOVED"),其中包含下一個表作爲其鍵。遇到轉發節點(forwarding node)時,使用新表重新啓動訪問和更新操作。

Each bin transfer requires its bin lock, which can stall waiting for locks while resizing. However, because other threads can join in and help resize rather than contend for locks, average aggregate waits become shorter as resizing progresses. The transfer operation must also ensure that all accessible bins in both the old and new table are usable by any traversal. This is arranged in part by proceeding from the last bin (table.length - 1) up towards the first. Upon seeing a forwarding node, traversals (see class Traverser) arrange to move to the new table without revisiting nodes. To ensure that no intervening nodes are skipped even when moved out of order, a stack (see class TableStack) is created on first encounter of a forwarding node during a traversal, to maintain its place if later processing the current table. The need for these save/restore mechanics is relatively rare, but when one forwarding node is encountered, typically many more will be. So Traversers use a simple caching scheme to avoid creating so many new TableStack nodes. (Thanks to Peter Levart for suggesting use of a stack here.)

每個 bin 轉換都需要其 bin 鎖(bin lock),該鎖在調整大小時可能會停止等待鎖。但是,由於其他線程可以加入並幫助調整大小,而不是爭用鎖,因此隨着調整大小的進展,平均聚合等待時間會變短。 轉換操作還必須確保舊錶和新表中的所有可訪問bin 都可供任何遍歷使用。 這是通過從最後一個 bin(table.length - 1)向上向第一個bin繼續部分安排的。 看到轉發節點(forwarding node)後,遍歷(請參閱類 Traverser)安排移動到新表,而無需重新訪問節點。 爲了確保即使移動順序不跳過任何中間節點,也會在遍歷期間第一次遇到轉發節點時創建一個堆棧(請參閱類 TableStack),以便在以後處理當前表時保持其位置。對這些保存/還原機制的需求相對較少,但當遇到一個轉發節點時,通常會遇到更多。 因此,遍歷者使用簡單的緩存方案來避免創建這麼多新的 TableStack 節點。(感謝 Peter Levart 建議在這裏使用堆疊。)

The traversal scheme also applies to partial traversals of ranges of bins (via an alternate Traverser constructor) to support partitioned aggregate operations. Also, read-only operations give up if ever forwarded to a null table, which provides support for shutdown-style clearing, which is also not currently implemented.

遍歷方案也適用於bins 範圍的部分遍歷(通過備用 Traverser 構造函數),以支持分區聚合操作(partitioned aggregate operations)。 此外,如果轉發到 null 表,則只讀操作將放棄,該表支持關閉式清除(shutdown-style clearing),而關閉式清除當前也沒有實現。

Lazy table initialization minimizes footprint until first use, and also avoids resizings when the first operation is from a putAll, constructor with map argument, or deserialization. These cases attempt to override the initial capacity settings, but harmlessly fail to take effect in cases of races.

延遲表初始化可最大程度地減少佔用空間,直到首次使用,並且還避免了當第一個操作來自 putAll、具有映射參數的構造函數或反序列化時調整大小。 這些情況試圖覆蓋初始容量設置,但在比賽情況下,這些設置不會無害地生效。

The element count is maintained using a specialization of LongAdder. We need to incorporate a specialization rather than just use a LongAdder in order to access implicit contention-sensing that leads to creation of multiple CounterCells. The counter mechanics avoid contention on updates but can encounter cache thrashing if read too frequently during concurrent access. To avoid reading so often, resizing under contention is attempted only upon adding to a bin already holding two or more nodes. Under uniform hash distributions, the probability of this occurring at threshold is around 13%, meaning that only about 1 in 8 puts check threshold (and after resizing, many fewer do so).

使用 LongAdder 的專門化維護元素計數。我們需要合併一個專業化,而不是僅僅使用LongAdder,以便訪問隱式爭用感應(implicit contention-sensing),導致創建多個計數器單元(CounterCells)。 計數器機制避免在更新上爭用,但如果在併發訪問期間讀取過於頻繁,則可能會遇到緩存抖動(cache thrashing)。爲了避免經常讀取,僅在添加到已包含兩個或多個節點的 bin 時,纔會嘗試在爭用項下調整大小。在統一哈希分佈下,在閾值處發生此情況的概率約爲 13%,這意味着只有大約 8 分之一的 puts 檢查閾值(調整大小後,這樣做的可能性更少)。

TreeBins use a special form of comparison for search and related operations (which is the main reason we cannot use existing collections such as TreeMaps). TreeBins contain Comparable elements, but may contain others, as well as elements that are Comparable but not necessarily Comparable for the same T, so we cannot invoke compareTo among them. To handle this, the tree is ordered primarily by hash value, then by Comparable.compareTo order if applicable. On lookup at a node, if elements are not comparable or compare as 0 then both left and right children may need to be searched in the case of tied hash values. (This corresponds to the full list search that would be necessary if all elements were non-Comparable and had tied hashes.) On insertion, to keep a total ordering (or as close as is required here) across rebalancings, we compare classes and identityHashCodes as tie-breakers. The red-black balancing code is updated from pre-jdk-collections (http://gee.cs.oswego.edu/dl/classes/collections/RBCell.java) based in turn on Cormen, Leiserson, and Rivest “Introduction to Algorithms” (CLR).

TreeBins 對搜索和相關操作使用特殊形式的比較(這是我們不能使用現有集合(如TreeMaps)的主要原因)。TreeBin 包含可比較的元素,但可能包含其他元素,以及對於同一 T 具有可比性但不一定可比較的元素,因此,我們無法在它們之間調用比較。爲了處理這種情況,tree 主要按哈希值排序(the tree is ordered primarily by hash value),然後按 Comparable.compareTo 順序(如果適用)排序。 在節點上查找時,如果元素無法比較或比較爲 0,則在綁定哈希值的情況下可能需要搜索左子級(left children)和右子級(right children)。(這對應於所有元素非可比較且已綁定哈希項時所需的完整列表搜索。在插入時,爲了在重新平衡中保持總排序(或按此處要求的那樣接近),我們將 classes 和 identityHashCodes 進行比較,將其作爲連接中斷器(tie-breakers)。紅黑平衡代碼從前 jdk 集合 (http://gee.cs.oswego.edu/dl/classes/collections/RBCell.java) 更新,依次基於 Cormen、Leiserson 和 Rivest “Introduction to Algorithms”(CLR)。

參考文檔

TreeBins also require an additional locking mechanism. While list traversal is always possible by readers even during updates, tree traversal is not, mainly because of tree-rotations that may change the root node and/or its linkages. TreeBins include a simple read-write lock mechanism parasitic on the main bin-synchronization strategy: Structural adjustments associated with an insertion or removal are already bin-locked (and so cannot conflict with other writers) but must wait for ongoing readers to finish. Since there can be only one such waiter, we use a simple scheme using a single “waiter” field to block writers. However, readers need never block. If the root lock is held, they proceed along the slow traversal path (via next-pointers) until the lock becomes available or the list is exhausted, whichever comes first. These cases are not fast, but maximize aggregate expected throughput.

樹箱還需要額外的鎖定機制。 雖然列表遍歷始終可以由讀者進行,即使在更新期間也是如此,但樹遍歷則不可能發生,這主要是因爲樹的旋轉可能會更改根節點和/或其鏈接。 TreeBins 包括一個簡單的讀寫鎖定機制,寄生在主 bin 同步策略上:與插入或刪除相關的結構調整已進行 bin 鎖定(因此不能與其他編寫器衝突),但必須等待正在進行的讀者完成。由於只能有一個這樣的侍者,我們使用一個簡單的方案使用單個"侍者"字段來阻止編寫器。 但是,讀者永遠不需要阻止。 如果根鎖被持有,它們將沿着慢速遍歷路徑(通過下一個指針)繼續,直到鎖變爲可用或列表用盡(以先到者爲準)。這些情況並不快,但可以最大化聚合預期吞吐量。

Maintaining API and serialization compatibility with previous versions of this class introduces several oddities. Mainly: We leave untouched but unused constructor arguments referring to concurrencyLevel. We accept a loadFactor constructor argument, but apply it only to initial table capacity (which is the only time that we can guarantee to honor it.) We also declare an unused “Segment” class that is instantiated in minimal form only when serializing.

與此類的早期版本保持 API 和序列化兼容性會帶來一些奇怪之處。主要:我們保留未受保留但未使用的構造函數參數,這些參數引用併發級別。我們接受 loadFactor 構造函數參數,但僅將其應用於初始表容量(這是我們唯一可以保證遵守它的時間)。我們還聲明瞭一個未使用的"Segment"類,該類僅在序列化時以最小形式實例化。

Also, solely for compatibility with previous versions of this class, it extends AbstractMap, even though all of its methods are overridden, so it is just useless baggage.

此外,僅爲了與此類的早期版本兼容,它繼承了AbstractMap,即使其所有方法都被重寫,所以它只是無用的行李。

This file is organized to make things a little easier to follow while reading than they might otherwise: First the main static declarations and utilities, then fields, then main public methods (with a few factorings of multiple public methods into internal ones), then sizing methods, trees, traversers, and bulk operations.

此文件是爲了讓在閱讀時更容易遵循:首先主靜態聲明和實用程序,然後是字段,然後是主公共方法(將多個公共方法的一些分解分解爲內部方法),然後調整方法、樹、遍歷者和批量操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章