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.

此文件是为了让在阅读时更容易遵循:首先主静态声明和实用程序,然后是字段,然后是主公共方法(将多个公共方法的一些分解分解为内部方法),然后调整方法、树、遍历者和批量操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值