从源码看Android常用的数据结构 ( SDK23版本 ) ( 六, ConcurrentHashMap )

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/j550341130/article/details/81364529

此系列文章放在了我的专栏里, 欢迎查看
https://blog.csdn.net/column/details/24187.html

相关衔接
从源码看Android常用的数据结构 ( SDK23版本 ) ( 一 , 总述 )
从源码看Android常用的数据结构 ( SDK23版本 ) ( 二, List篇 )
从源码看Android常用的数据结构 ( SDK23版本 ) ( 三 , Queue篇)
从源码看Android常用的数据结构 ( SDK23版本 ) ( 四, Set篇 )
从源码看Android常用的数据结构 ( SDK23版本 ) ( 五, Map篇 )
从源码看Android常用的数据结构 ( SDK23版本 ) ( 六, ConcurrentHashMap )

Github里有一份Android使用到的小技术, 欢迎查看:
https://github.com/YouCii/LearnApp


前言

由于从源码看Android常用的数据结构 ( SDK23版本 ) ( 五, Map篇 ) 篇幅已经很长了, ConcurrentHashMap 要看的又比较多, 所以单独放在这里.

ConcurrentHashMap致力于高效率的并发, 好像Spring的底层数据结构就是使用的ConcurrentHashMap.
为了实现并发的高效率, ConcurrentHashMap做了大量优化: 更为细化的分段锁(数组的每个容器的头节点作为锁), 无锁的读操作, 并发扩容等等.

此版的ConcurrentHashMapHashMap思想差不多, 都是数组内存储链表(红黑树)的存储方式.
很多资料都在说Segment, 这个概念是在早期版本中出现的概念(与concurrencyLevel相关), 现在已经没有了, 现在ConcurrentHashMap中的单独锁是锁住的头节点Node. 请见此说明:

    /**
     * Stripped-down version of helper class used in previous version,
     * declared for the sake of serialization compatibility
     * 在先前版本中使用的精简版辅助类,为了序列化兼容性而声明
     */
    static class Segment<K, V> extends ReentrantLock implements Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
        final float loadFactor;

        Segment(float lf) {
            this.loadFactor = lf;
        }
    }

ConcurrentHashMap 类声明如下:

public class ConcurrentHashMap<K, V> 
        extends AbstractMap<K, V>
        implements ConcurrentMap<K, V>, Serializable {

这里实现了 ConcurrentMap 接口, 字面意思: 并发Map. 我们先看一下这个接口, 有助于后面ConcurrentHashMap的理解.


ConcurrentMap接口说明

ConcurrentMap提供了类似于CAS的几个读写方法, 用于实现原子操作( 其实 ConcurrentHashMap 中的这几个方法就是用 Unsafe.CAS 实现的).

A {@link java.util.Map} providing additional atomic
{@code putIfAbsent}, {@code remove}, and {@code replace} methods.

<p>Memory consistency effects: As with other concurrent
collections, actions in a thread prior to placing an object into a
{@code ConcurrentMap} as a key or value
<a href="package-summary.html#MemoryVisibility"><i>happen-before</i></a>
actions subsequent to the access or removal of that object from
the {@code ConcurrentMap} in another thread.

ConcurrentMap是一种额外提供原子性的putIfAbsent/remove/replace方法的Map.
内存一致性原则: 像其他并发集合一样, 较先发生在某线程中的写入操作 happen-before 在
另一个线程中较后进行的读取操作.

注: happen-before 讲解请见我的另一篇转载博客 
https://blog.csdn.net/j550341130/article/details/80774359

下面看下每个方法, 方法注释里给予了类似逻辑的代码解释, 非常清晰, 所以就不再贴文字说明了.

putIfAbsent 方法

/**
 * if (!map.containsKey(key))
 *     return map.put(key, value);
 * else
 *     return map.get(key);}
 */
V putIfAbsent(K key, V value);

remove 方法

/**
 * if (map.containsKey(key) && map.get(key).equals(value)) {
 *     map.remove(key);
 *     return true;
 * } else {
 *     return false;
 * }
 */
boolean remove(Object key, Object value);

replace 方法

/**
 * if (map.containsKey(key) && map.get(key).equals(oldValue)) {
 *     map.put(key, newValue);
 *     return true;
 * } else
 *     return false;}
 */
boolean replace(K key, V oldValue, V newValue);

/**
 * if (map.containsKey(key)) {
 *   return map.put(key, value);
 * } else {
 *   return null;
 * }
 */
V replace(K key, V value);

ConcurrentHashMap 类注释

介绍了 ConcurrentHashMap 特性.

A hash table supporting full concurrency of retrievals and
high expected concurrency for updates. This class obeys the
same functional specification as {@link java.util.Hashtable}, and
includes versions of methods corresponding to each method of
{@code Hashtable}. However, even though all operations are
thread-safe, retrieval operations do <em>not</em> entail locking,
and there is <em>not</em> any support for locking the entire table
in a way that prevents all access.  This class is fully
interoperable with {@code Hashtable} in programs that rely on its
thread safety but not on its synchronization details.

一种支持检索完全并发/更新高期望并发性的hash表. 此类遵循与HashTable相一致的
功能规范, 并且包含了与HashTable内每一个方法相对应的不同版本的方法. 不过, 即
使所有的操作都是线程安全的, 检索操作也不会强制锁定, 并且也没有任何支持会锁住
整个表来阻止访问. 此类在只要求线程安全不要求同步细节实现的场景下完全可以与
Hashtable通用.

<p>Retrieval operations (including {@code get}) generally do not
block, so may overlap with update operations (including {@code put}
and {@code remove}). Retrievals reflect the results of the most
recently <em>completed</em> update operations holding upon their
onset. (More formally, an update operation for a given key bears a
<em>happens-before</em> relation with any (non-null) retrieval for
that key reporting the updated value.)  For aggregate operations
such as {@code putAll} and {@code clear}, concurrent retrievals may
reflect insertion or removal of only some entries.  Similarly,
Iterators and Enumerations return elements reflecting the state of
the hash table at some point at or since the creation of the
iterator/enumeration.  
They do <em>not</em> throw {@link
ConcurrentModificationException}.  However, iterators are designed
to be used by only one thread at a time.  Bear in mind that the
results of aggregate status methods including {@code size}, {@code
isEmpty}, and {@code containsValue} are typically useful only when
a map is not undergoing concurrent updates in other threads.
Otherwise the results of these methods reflect transient states
that may be adequate for monitoring or estimation purposes, but not
for program control.

get等检索操作通常不会锁住, 所以有可能与put/remove等更新操作同时发生. 检索操作
只会反映最近已经执行完毕的更新操作后的值(更正式的说法是, 一个key的更新操作会
happen-before于这个对应value的key的任何检索操作). 对于像putAll.clear之类的
聚合操作, 并发的检索结果可能只能反映其中的部分插入和删除. 同样, 迭代器和枚举器
返回的元素也会反映自身创建时的Hash表的状态.
迭代器和枚举器不会抛出ConcurrentModificationException异常. 迭代器被设计用于
单线程下使用. 注意, 包括size()/isEmpty()/containsValue()等聚合状态的方法返回
结果只有当map没有在其他线程被并发修改时才是有用的. 除非这些方法的结果只用于监测和
估计目的, 不用于程序控制.

<p>The table is dynamically expanded when there are too many
collisions (i.e., keys that have distinct hash codes but fall into
the same slot modulo the table size), with the expected average
effect of maintaining roughly two bins per mapping (corresponding
to a 0.75 load factor threshold for resizing). There may be much
variance around this average as mappings are added and removed, but
overall, this maintains a commonly accepted time/space tradeoff for
hash tables.  However, resizing this or any other kind of hash
table may be a relatively slow operation. 
When possible, it is a good idea to provide a size estimate as an
optional {@code initialCapacity} constructor argument. An additional 
optional {@code loadFactor} constructor argument provides a further 
means of customizing initial table capacity by specifying the table
density to be used in calculating the amount of space to allocate for
the given number of elements.  
Also, for compatibility with previous versions of this class, constructors
may optionally specify an expected {@code concurrencyLevel} as an additional
hint for internal sizing.  Note that using many keys with exactly the same
{@code hashCode()} is a sure way to slow down performance of any
hash table. To ameliorate impact, when keys are {@link Comparable},
this class may use comparison order among keys to help break ties.

此表会在hash碰撞过多时自动扩容(不同hash值的keys存入了同一index的链表中), 预期的
是每个映射平均保存两个元素(对应于用来调整大小的0.75负载因子). 当添加/移除映射时具
体数值可能与平均值有很大差异, 但大体上保持了哈希表的时间/空间效率平衡. 然而, 重新
调整大小对此类以及其他类型的hash表可能都是一个相对缓慢的操作. 
如果可能的话, 把估算的预定大小作为构造方法的参数传入(initialCapacity)是个不错的办
法. 另一个构造参数loadFactor提供了进一步的自定义表初始大小的方法, 通过分配用来计算
指定数目元素的空间大小的表密度.
另外, 为了与此类的早期版本兼容, 构造方法可以传入一个预期并发级别用于内部大小的额外提示.
注意使用hashCode完全相同的keys必定会减慢任意hash表的性能. 为了降低此风险, 如果key是
可比较的(即继承了Comparable), 此类可以使用各个key的比较顺序来辅助区分他们.

Overview 概述(翻译的不好, 随便一看吧)

这一部分主要讲述了ConcurrentHashMap的设计思想及具体实现机制.

 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.

 此Hash表的主要设计目标是保持并发可读性(特别是get方法, 也包括迭代器及相关
 方法), 并同时最小化update竞争. 次要目标是保持与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.)

 这个映射通常充当一个装箱(散列)哈希表. 每个键值对均保存在一个Node实例中. 
 大多数node都是持有hash/key/value/下一个node的基本Node类实例, 不过也有
 不同的子类存在: TreeNodes的被存放在平衡树中而不是列表, 叶子节点持有根节
 点的实例; ForwardingNodes在重新调整大小时会放置在容器的头部; 
 ReservationNodes 在computeIfAbsent相关方法中设定键值时被用作占位符.
 这三个子类均不持有普通的keys/values/hashes, 在查询等操作时也非常容易辨
 认, 因为它们的hash值是负数, key/value均为null(这些特殊的node要么是特殊
 的, 要么是瞬态的, 所以持有一些无用字段影响不大, 个人认为指的是内存影响).

 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
 (sun.misc.Unsafe) operations.

 此表在第一次插入时容量会懒加载为双倍大小. 表内的每一个节点容器都包含多个node
 (通常是0~1个). 表访问需要易变的/原子的读写方法以及CSA方法. 因为没有其他可
 以不添加额外接口满足此要求的方法, 我们使用了底层操作(sun.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的hash字段的顶部位(符号位)来实现控制目的, 因为寻址约束的原因
 这种方式非常可靠. hash值是负数的Node在各个Map方法中会被特殊处理或者直接
 忽略;

 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.

 每个空容器的第一个元素的插入操作(put以及其变型方法)只会通过cas方法插入.
 这是在大多数密钥/散列分布场景下的最常见的操作. 而其他的更新操作(insert/
 delete/replace)则需要加锁. 我们不希望使用一把显式锁关联每一个bin容器导
 致空间浪费, 替代方案是使用每个bin容器自己的第一个node作为锁对象. 锁是使
 用synchronized实现的.

 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).

 使用列表的第一个节点作为锁本身并不足够: 当一个结点被锁住时, 任何更新操作
 都必须确认它在释放锁后仍是第一个结点, 如果不是则重试. 因为新的结点总是被
 追加到列表中, 所以只要一个结点成为了容器中的第一个, 它就会一直保持在第一
 位直到被删除或者其容器变为不可用(调整大小后).

 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)). 

 这种把每个bin作为锁的主要缺点是 被相同的锁保护的容器列表内的其他元素的
 其他更新操作会有阻塞, 例如当equals()或者其他映射方法需要很长时间时. 不过
 统计显示, 使用随机散列值时此问题并不常见. 在理想情况下, 参数平均0.5/重
 新调整大小阈值0.75时, 容器中的节点的频率遵循泊松分布, 尽管不同的粒度会
 导致较大的不同. 忽略方差, 列表大小的期望是 :
 exp(-0.5) * pow(0.5, k) / factorial(k)

 The first values are:
 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.

 实际上散列码分布有时明显偏离均匀随机性. 这包括当N>(1<<30)时, 某些键必须
 碰撞. 类似的情况还有多个键被设计为相同的hash值/仅有一个符号位不同的错误
 /敌意用法. 因此, 当容器中的节点数超过阈值时我们使用了次要策略. 这些树集
 使用平衡树来保存节点(一种特殊形式的红黑树), 此集合的查询时间复杂度为
 O(log n). 树列表中的每个搜索步骤至少比常规列表慢两倍, 但是假设n不能超过
 (1<<64)(在内存地址用完之前), 那么搜索范围/锁定时间等都是合理的(最坏的情况
 是每次操作查询100个节点), 只要键是Comparable 的(很常见的特性). TreeBin节
 点(树节点)也持有相同的"next"遍历指针作为常规节点, 因此可以以相同的方式遍历
 迭代器.

 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.
 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 collectable 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.

 当占用率超过百分比阈值(名义上为0.75,但见下文)时,存储表会调整大小。 在
 启动线程分配和设置替换阵列之后,任何注意到过某已满bin的线程都可以帮助调整大
 小。然而此时并没有卡顿,其他线程可以继续插入等操作。使用TreeBins避免
 了重新调整大小的过程中过度填充的最坏情况的影响. 调整大小时会将bin逐个从一
 个表转移到下一个表中。但是,线程在这样做之前要求小的索引块进行转移(通过
 transferIndex字段),以便减少争用。sizeCtl字段的标记可以确保不会重复调整
 大小。因为我们扩展遵循2的次幂, 所以每个bin要么保持原index, 要么以2的n次幂
 的距离转移。我们通过捕获旧节点可以重用的对象来消除不必要的节点创建,因为它们
 的next字段不会改变。平均来说,当容器翻倍时只有大约六分之一需要克隆。它们替
 换的节点一旦不再被任何读取线程引用(这些线程有可能仍在表遍历过程中),就会被
 gc回收。在传输时,旧表的bin只包含一个特殊的转发节点(hash字段为“MOVED”),
 这个转发节点包含下一个表作为其key。在遇到转发节点时,访问和更新操作重新启
 动,并使用新表。

 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都可以被任何遍历使
 用。这部分是通过从最后一个bin(table.length - 1)向前到第一个bin来安排的。
 在看到转发节点时,遍历(请参阅类Traverser)安排移动到新表而不重新访问节点。
 为了确保即使在不按顺序移动时也不会跳过中间节点,在遍历期间首次遇到转发节点时
 会创建堆栈(请参阅类TableStack),以便在以后处理当前表时保持其位置。对这些
 保存/恢复机制的需求相对较少,但是当遇到一个转发节点时,通常会有更多转发节点。
 所以Traversers使用一个简单的缓存方案来避免创建这么多新的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.

 此遍历方案也适用于遍历某一部分bin的区域遍历(看一下交替遍历构造函数),以支
 持分区聚合操作。此外,只读操作如果被转发到一个空表时会放弃操作,这为目前还未
 实现的关机样式清除提供支持。

 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()/传入map的构造方法/反序列化等操作的重新调整大小操作. 这些事例
 试图覆盖初始容量设置, 但是在竞争场景下会无害的失效.

 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来访问隐式的竞争感知(这可能导致CounterCells的重复创建). 计数器
 机制避免的更新操作时的并发竞争, 但是如果在并发访问期间过于频繁地读取,则会遇到
 缓存抖动. 为了避免如此频繁的读操作,仅在向已经持有两个或更多节点的bin中添加
 时才尝试在竞争时调整大小. 在统一的哈希分布下, 这种情况在特性阈值发生的可能
 性大约为13%, 这意味着只有大约1/8的put操作会检查阈值(并且在调整大小之后,这样
 做的可能性会更小).

 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).

 这里的TreeBin在查找及相关操作中使用了一种特殊形式的比较方法(这是不能使用例如
 TreeMaps等已存在集合的主要原因). TreeBin中包含可比较的元素, 不过也包含其他
 不可比较的类型, 还有的元素虽然本身是可比较的, 但其相同类的其他实例却不一定也是
 可比较的, 所以我们不能在它们之间调用compareTo方法. 为了处理此情况, 树结构首先
 通过hash值排序, 其次才是Comparable.compareTo顺序(如果可用的话). 搜索某节点
 时, 如果元素不是可比较的或者比较结果为0, 那么hash值碰撞场景下可能需要同时查询
 左右两个子节点(这意味着如果所有的元素都是不可比较的并且hash全碰撞, 则需要进行
 整个列表的查询). 为了在插入操作时通过重新实现树结构保持全排序或者尽量接近全排序,
 我们在元素比较时把类和identityHashCodes作为了联系打破者. 红黑平衡代码来自
 pre-jdk-collections, 它是基于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需要一种额外的锁机制. 列表在update过程中仍然可以遍历, 但树不行,
 这主要是因为树旋转操作可能会改变根节点及其链接. TreeBins包含一个依赖于
 主Bin同步策略的简单读写锁机制: 与插入/移除操作相关的结构调整会被Bin锁锁住
 (所以不会与其他写操作冲突), 必须等待正在进行的读操作完成. 为了保证只能存在
 一个等待线程, 我们采用了一种简单的方式, 即使用了一个单独的waiter字段来阻塞
 其他的写操作. 不过读操作永远不会被阻塞. 如果根节点被锁住, 这些读操作会延着
 慢速遍历路径(通过下一个指针)进行, 直到锁被释放或者列表为空, 以先到者为准.
 这样的机制运行不算快, 但是最大化了期望的总吞吐量.

 Maintaining API and serialization compatibility with previous
 versions of this class introduces several oddities. Mainly: We
 leave untouched but unused constructor arguments refering 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和序列化时引入了一些奇怪的地方。主要是:我们保留了
 未使用的构造参数,例如concurrencyLevel。 接受一个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,它的所有方法都
 被重写了,因此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.

 这个文件被重新调整过,使得阅读时追溯代码比其他方式更容易:首先是主要的静态声
 部方法),然后是调整大小相关方法,树,遍历和批量操作等。

核心方法源码

主要是 Map 接口 的put/get/remove以及 ConcurrentMap 接口的putIfAbsent/remove/replace方法.

其中

  • Map.putConcurrentMap.putIfAbsent都是调用的final V putVal(K key, V value, boolean onlyIfAbsent);
  • Map.removeConcurrentMap.remove/replace都是调用的final V replaceNode(Object key, V value, Object cv).

下面看下final V putVal(K key, V value, boolean onlyIfAbsent) final V replaceNode(Object key, V value, Object cv) Map.get 三个方法.

  • final V putVal(K key, V value, boolean onlyIfAbsent)

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        // 计算Hash值
        int hash = spread(key.hashCode());
        // 用于记录某节点链表的元素数目, 超过TREEIFY_THRESHOLD(8)时把链表转化为树结构
        int binCount = 0;
        for (Node<K, V>[] tab = table; ; ) {
            // 目前根节点的数据
            Node<K, V> f;
            // n是tab.length, i是根节点的内存地址, fh根节点hash值
            int n, i, fh;
            if (tab == null || (n = tab.length) == 0) {
                // 如果集合为空, 首先初始化一下
                tab = initTable();
            } else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // f设置为根节点的数据
                if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null))) {
                    // 如果根节点没有数据, cas方法插入该位置的根节点
                    break;
                }
            } else if ((fh = f.hash) == MOVED) {
                // 如果根节点的hash值被标记为moved, 转移到新的列表中继续遍历
                tab = helpTransfer(tab, f);
            } else { // 根节点不是空/也没有被标记为moved, 是正常值的话
                V oldVal = null;
                // 锁住根节点(为什么这么做在概述中有说明)
                synchronized (f) {
                    // 再校验一下根节点是不是还是f对象
                    if (tabAt(tab, i) == f) {
                        // 如果hash>0
                        if (fh >= 0) {
                            binCount = 1;
                            // 遍历该位置的链表或者红黑树
                            for (Node<K, V> e = f; ; ++binCount) {
                                K ek;
                                // 如果key值完全一样, 替换value
                                if (e.hash == hash && 
                                    ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent) e.val = value;
                                    break;
                                }
                                Node<K, V> pred = e;
                                // 不一样的话移到下一个链表节点, 如果没有下一个节点了, 把传入的value放在此处
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K, V>(hash, key, value, null);
                                    break;
                                }
                            }
                        } else if (f instanceof TreeBin) {
                            // 如果hash<0的话, 应该是树节点, 通过TreeBin.putTreeVal进行插入操作.
                            Node<K, V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K, V>) f).putTreeVal(hash, key, value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                // 如果该节点下的链表长度超过8了, 转化为树结构
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        // 这个方法内部把所有的节点转化为了TreeBin:
                        // setTabAt(tab, index, new TreeBin<K, V>(hd));
                        treeifyBin(tab, i); 
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 增加数目. 如果没有重新调整大小, 那么开始转移; 如果开始了调整大小, 帮助一同调整.
        addCount(1L, binCount);
        return null;
    }
  • final V replaceNode(Object key, V value, Object cv)

    final V replaceNode(Object key, V value, Object cv) {
        int hash = spread(key.hashCode());
        for (Node<K, V>[] tab = table; ; ) {
            // 目前根节点数据
            Node<K, V> f;
            // n是tab.length, i是根节点内存地址, fh根节点hash值
            int n, i, fh;
            if (tab == null || (n = tab.length) == 0 || (f = tabAt(tab, i = (n - 1) & hash)) == null)
                // 集合状态不可用直接退出返回null
                break;
            else if ((fh = f.hash) == MOVED)
                // 如果根节点的hash值被标记为moved, 转移到新的列表中继续遍历
                tab = helpTransfer(tab, f);
            else {
                // 根节点不是空/也没有被标记为moved, 是正常值的话, 开始遍历查询
                V oldVal = null;
                // 是否通过验证, 如果替换为了null, 集合大小要减一
                boolean validated = false;
                // 锁住根节点(为什么这么做在概述中有说明)
                synchronized (f) {
                    // 再校验一下根节点是不是还是f对象
                    if (tabAt(tab, i) == f) {
                        // 如果hash>0
                        if (fh >= 0) {
                            validated = true;
                            // 遍历该位置的链表或者红黑树
                            for (Node<K, V> e = f, pred = null; ; ) {
                                K ek;
                                // 如果key值完全一样
                                if (e.hash == hash && 
                                    ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                                    V ev = e.val;
                                    // 检验旧数据是否是传入的cv
                                    if (cv == null || cv == ev || (ev != null && cv.equals(ev))) {
                                        oldVal = ev;
                                        if (value != null)
                                            e.val = value;
                                        else if (pred != null)
                                            pred.next = e.next;
                                        else
                                            setTabAt(tab, i, e.next);
                                    }
                                    break;
                                }
                                pred = e;
                                // 不一样的话移到下一个链表节点, 如果没有下一个节点了, 说明没查到
                                if ((e = e.next) == null)
                                    break;
                            }
                        } else if (f instanceof TreeBin) {
                            // 如果hash<0的话, 应该是树节点, 通过TreeNode.findTreeNode查找.
                            validated = true;
                            TreeBin<K, V> t = (TreeBin<K, V>) f;
                            TreeNode<K, V> r, p;
                            if ((r = t.root) != null && (p = r.findTreeNode(hash, key, null)) != null) {
                                V pv = p.val;
                                // 与上方基本一致的逻辑
                                if (cv == null || cv == pv || (pv != null && cv.equals(pv))) {
                                    oldVal = pv;
                                    if (value != null)
                                        p.val = value;
                                    else if (t.removeTreeNode(p))
                                        setTabAt(tab, i, untreeify(t.first));
                                }
                            }
                        }
                    }
                }
                // 如果替换为了null, 集合大小要减一
                if (validated) {
                    if (oldVal != null) {
                        if (value == null)
                            addCount(-1L, -1);
                        return oldVal;
                    }
                    break;
                }
            }
        }
        return null;
    }
  • Map.get 与上两个方法相比就比较简单了

    public V get(Object key) {
        Node<K, V>[] tab;
        Node<K, V> e, p;
        int n, eh;
        K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) {
            // 恰好旧是根节点
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            // 使用树节点TreeBin的find方法来查询
            else if (eh < 0) {
                return (p = e.find(h, key)) != null ? p.val : null;
            }
            // 普通链表
            while ((e = e.next) != null) {
                if (e.hash == h &&
                        ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

其他内部类/方法/变量

  1. initTable初始化数据表

    private final Node<K, V>[] initTable() {
        Node<K, V>[] tab;
        int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                // 如果已经在初始化了, 就放弃竞争
                Thread.yield();
            // cas方法尝试把SIZECTL标记为已在初始化的状态, 然后进入初始化
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K, V>[] nt = (Node<K, V>[]) new Node<?, ?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }
    
  2. sizeCtl 表大小控制变量

     /**
     * Table initialization and resizing control.  When negative, the
     * table is being initialized or resized: -1 for initialization,
     * else -(1 + the number of active resizing threads).  Otherwise,
     * when table is null, holds the initial table size to use upon
     * creation, or 0 for default. After initialization, holds the
     * next element count value upon which to resize the table.
     * 
     * 表初始化和调整大小控制. 当为负数时, 表正在初始化或调整大
     * 小: -1表示初始化, -N 表示有N-1个线程正在进行扩容操作. 另外当table
     * 为null时, 其值为初始化时会使用的初始表大小, 或者默认为0. 初始
     * 化之后, 持有会激发重新调整大小的元素count。
     */
    private transient volatile int sizeCtl;
    
  3. Node, TreeNode, TreeBin:

    /**
     * 不支持用户setVaule操作的键值对, 可以进行只读遍历. hash
     * 值是负数的Node子类比较特殊, key/value可以为null, 其他情况
     * 下不能为空.
     * 注: 1. setVaule 直接 throw Exception; 
     *     2. 提供了Node<K, V> find(int h, Object k) 方法支持读操作.
     */
    static class Node<K, V> implements Map.Entry<K, V>
    
    /**
     * Node的一个子类, 当链表长度过长时Node会转换为TreeNode. 但TreeNode
     * 并不是红黑树,TreeNode会放在TreeBin中, 由TreeBin实现红黑树.
     * .....其实不太明白为什么要有TreeNode这一层.
     */
    static final class TreeNode<K, V> extends Node<K, V>
    
     /**
     * 用作集合数组每个表头节点的TreeNode. TreeBin中不存储key/value, 而是
     * 指向了一个TreeNode列表以及此列表的TreeNode root. TreeBin也持有了寄
     * 生的读写锁, 用于强制 持有锁的写操作在树结构重新调整之前等待无锁的读
     * 操作完成后再执行.
     * 注: TreeBin使用了固定的hash值: static final int TREEBIN = 0x80000000. 构
     *    造方法也是红黑树的构造形式.
     */
    static final class TreeBin<K, V> extends Node<K, V>

    另外, 根据类概述, ForwardingNodes在重新调整大小时会放置在容器的头部, ReservationNodescomputeIfAbsent相关方法中设定键值时被用作占位符.

  4. 扩容操作private final void transfer(Node<K, V>[] tab, Node<K, V>[] nextTab)
    transfer的扩容思想与HashMap类似, 但是支持并发扩容, 所以更加复杂一些. 源码就不看了, 比较复杂, 主要思想大体是这样:

    1. 利用RESIZE_STAMP_SHIFT确保单线程创建nextTab(..这里怎么做的我没有看懂, ⊙﹏⊙);
    2. tab中的元素复制到nextTab: 多线程复制节点时处理过一个节点后就把对应的Node设置为ForwardingNode, 当其他线程判断到此节点为ForwardingNode时, 就会跳过此节点继续向后遍历. 这样不仅实现了并发操作, 而且还提高了效率.

猜你喜欢

转载自blog.csdn.net/j550341130/article/details/81364529