深入浅出 JAVA 之集合 - ConcurrentHashMap
文章目录
!版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。
系统环境:
- JDK 版本: OpenJDK 8
一、ConcurrentHashMap 概述
ConcurrentHashMap 和 HashMap 一样,是一个实现了 Map 接口,用于存储键值对数据的集合。
不过 ConcurrentHashMap 和 HashMap 又不完全相同,比如 ConcurrentHashMap 相对于 HashMap 具有更好的线程安全性和并发性能,并提供了一些额外的并发操作方法和迭代器的一致性保证,可以在多线程环境下保证集合的线程安全。然而也正是由于引入了额外的同步机制,ConcurrentHashMap 的性能可能略低于 HashMap。
所有,ConcurrentHashMap 可以简单的认为是一个和 HashMap 类似的线程安全的集 Map 集合,在选择使用 ConcurrentHashMap 还是 HashMap 时,可以根据具体的使用场景和并发需求进行综合考虑。
二、ConcurrentHashMap 底层结构
在 JDK 8 中,ConcurrentHashMap 底层结构和 HashMap 相同,都是采用了 数组 (table)
、链表
和 红黑树
组成。其结构如下图所示:
数组 table 是由一个个 桶
组成,所以也常称数组为 桶数组
。并且每个 桶
都关联了一个 链表
或者 红黑树
结构,这主要是用于解决哈希表结构中的哈希冲突。其中链表使用 Node
类型节点组成,红黑树则使用 TreeNode
节点组成,两种类型节点中都存储了键值对的 key
、value
以及 hash
信息,并且每个节点还使用 next
属性进行关联。
三、ConcurrentHashMap 属性
3.1 ConcurrentHashMap 中的默认常量值
1/**
2 * 集合最大容量限制.
3 */
4private static final int MAXIMUM_CAPACITY = 1 << 30;
5
6/**
7 * 默认初始容量。该值必须是 2 的 n 次幂,默认值为 16
8 */
9private static final int DEFAULT_CAPACITY = 16;
10
11/**
12 * 数组的最大长度
13 */
14static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
15
16/**
17 * 默认的并行度,为了兼容 JDK7 版本中的 Segment
18 */
19private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
20
21/**
22 * 扩容因子,当元素个数达到0.75*数组长度的时候触发扩容,也是jdk1.7使用,jdk1.8直接使用位操作计算出扩容阈值
23 */
24private static final float LOAD_FACTOR = 0.75f;
25
26/**
27 * 桶中-链表转换为红黑树的阈值
28 */
29static final int TREEIFY_THRESHOLD = 8;
30
31/**
32 * 桶中-红黑树还原为链表的阈值
33 */
34static final int UNTREEIFY_THRESHOLD = 6;
35
36/**
37 * 链表转化为红黑树时对集合最小容量的限制值,如果集合容量小于该值则链表不会转换为红黑树
38 */
39static final int MIN_TREEIFY_CAPACITY = 64;
40
41/**
42 * 并发扩容时每个线程最少处理16个桶
43 */
44private static final int MIN_TRANSFER_STRIDE = 16;
45
46/**
47 * 这三个常量控制并发扩容的结束,其实就是开始扩容的时候设置sizeCtl成一个负数,每次加入一个线程去扩容就sizeCtl++,
48 * 每次一个线程扩容结束就sizeCtl–-,那么当所有线程扩容结束后sizeCtl就等于最开始设置的那个负数
49 */
50private static int RESIZE_STAMP_BITS = 16;
51private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
52private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
53
54/**
55 * 当node节点的hash值 为-1时,表示当前节点为FWD节点
56 */
57static final int MOVED = -1;
58
59/**
60 * 当node节点的hash值 为-2时, 表示当前节点已经树化,且当前节点为TreeBin对象,TreeBin对象代理操作红黑树
61 */
62static final int TREEBIN = -2;
63
64/**
65 * 标识为ReservationNode节点(hash=-3),用于map.compute操作
66 */
67static final int RESERVED = -3;
68
69/**
70 * 可以将一个负数通过位与运算后得到正数,但是不是取绝对值
71 */
72static final int HASH_BITS = 0x7fffffff;
3.2 ConcurrentHashMap 中的成员变量
1/**
2 * 系统CPU的数量
3 */
4static final int NCPU = Runtime.getRuntime().availableProcessors();
5
6/**
7 * .散列表,长度一定是2次方数
8 */
9transient volatile Node<K,V>[] table;
10
11/**
12 * 扩容过程中,会将扩容中的新table 赋值给nextTable 保持引用,扩容结束之后,这里会被设置为null
13 */
14private transient volatile Node<K,V>[] nextTable;
15
16/**
17 * 未发生竞争时,或者当前LongAdder 处于加锁状态时,增量累加到baseCount
18 */
19private transient volatile long baseCount;
20
21/**
22 * sizeCtl < 0
23 * 1. -1 表示当前table正在初始化(有线程在创建table数组),当前线程需要自旋等待
24 * 2.表达当前map正在进行扩容 高16位表示:扩容的标识戳 低16位: 表示 1+N Thread 当前参与并发扩容的线程数量
25 *
26 * sizeCtl = 0;
27 * 表示创建map时,使用DEFALUT_CAPACITY 为大小
28 *
29 * sizeCtl > 0;
30 * 1.如果table未初始化,表示初始化大小;
31 * 2.如果table已经初始化了,表示下次扩容时触发条件(阈值)
32 */
33private transient volatile int sizeCtl;
34
35/**
36 * 扩容过程中,记录当前进度。所有线程都需要从transferIndex中分配区间任务,去执行自己的任务
37 */
38private transient volatile int transferIndex;
39
40/**
41 * LongAdder中的 cellsBuzy 0表示当前LongAdder对象无锁状态,1表示当前LongAdder对象加锁状态
42 */
43private transient volatile int cellsBusy;
44
45/**
46 * LongAdder 中的cells数组,当baseCount发生竞争后,会创建cells数组.线程会通过计算hash值取到自己的cell,将增量累加到指定cell中
47 */
48private transient volatile CounterCell[] counterCells;
四、ConcurrentHashMap 关键方法
4.1 构造方法 ConcurrentHashMap
(1) 无参构造方法
1/**
2 * 无参构造方法
3 */
4public ConcurrentHashMap() {
5}
(2) 支持配置初始化容量的构造方法
1/**
2 * 支持配置初始化容量的构造方法
3 *
4 * @param initialCapacity 初始化容量
5 * @throws IllegalArgumentException 非法参数异常
6 */
7public ConcurrentHashMap(int initialCapacity) {
8 if (initialCapacity < 0)
9 throw new IllegalArgumentException();
10 int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
11 MAXIMUM_CAPACITY :
12 tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
13 this.sizeCtl = cap;
14}
(3) 支持配置初始化容量与负载因子的构造方法
支持配置初始化容量与负载因子的构造方法,可以配置 初始化容量
和 负载因子
,不过在对参数进行配置前需要对参数进行一系列校验。
HashMap(int initialCapacity, float loadFactor)
1/**
2 * 支持配置初始化容量与负载因子的构造方法
3 *
4 * @param initialCapacity 初始化容量
5 * @param loadFactor 负载因子
6 * @throws IllegalArgumentException 非法参数异常 (如果初始容量为负数或者负载因子为负数,则抛出该异常)
7 */
8public HashMap(int initialCapacity, float loadFactor) {
9 // 如果初始化容量为负数,则抛出 IllegalArgumentException 异常
10 if (initialCapacity < 0) {
11 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
12 }
13 // 如果初始化容量大于 MAXIMUM_CAPACITY 值,则就设置初始化容量为 MAXIMUM_CAPACITY
14 if (initialCapacity > MAXIMUM_CAPACITY) {
15 initialCapacity = MAXIMUM_CAPACITY;
16 }
17 // 如果负载因子为负数,或者非数字 (NaN),则抛出 IllegalArgumentException 异常
18 if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
19 throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
20 }
21 // 设置【负载因子】和【扩容阈值】变量
22 this.loadFactor = loadFactor;
23 this.threshold = tableSizeFor(initialCapacity);
24}
tableSizeFor(int c)
1/**
2 * 返回一个大于等于且最接近 cap 的 2 的幂次方:
3 * - 比如传入初始化容量 cap = 9,则返回 2^4,即 16
4 * - 比如传入初始化容量 cap = 25,则返回 2^5,即 32
5 *
6 * @param cap 传入构造方法中初始化容量
7 * @return 传入初始化容量 cap 的 2 的幂次方
8 */
9private static final int tableSizeFor(int c) {
10 int n = c - 1;
11 n |= n >>> 1;
12 n |= n >>> 2;
13 n |= n >>> 4;
14 n |= n >>> 8;
15 n |= n >>> 16;
16 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
17}
(4) 支持根据已有 Map 集合创建新集合的构造方法
ConcurrentHashMap(Map<? extends K, ? extends V> m)
1/**
2 * 支持根据已有 Map 集合创建新集合的构造方法
3 *
4 * @param m 已有的 Map 集合
5 * @throws NullPointerException 空指针异常
6 */
7public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
8 this.sizeCtl = DEFAULT_CAPACITY;
9 putAll(m);
10}
putAll(Map<? extends K, ? extends V> m)
1/**
2 * 将插入的 Map 集合中的数据转移到当前集合中
3 *
4 * @param m 需要将数据插入到新集合中的 Map 集合
5 */
6public void putAll(Map<? extends K, ? extends V> m) {
7 tryPresize(m.size());
8 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
9 putVal(e.getKey(), e.getValue(), false);
10 }
11}
4.2 初始化数组方法 initTable()
initTable()
1/**
2 * 初始化数组
3 *
4 * @return 初始后的数组
5 */
6private final Node<K,V>[] initTable() {
7 Node<K,V>[] tab; // 记录待初始的数组的变量
8 int sc; // 记录 sizeCtl 值的变量
9
10 // 循环执行初始化操作
11 while ((tab = table) == null || tab.length == 0) {
12 // 设置变量 sc = sizeCtl,然后判断 sc 变量大小是否小于 0:
13 // - 如果 sc = -1,则说明有其他线程在执行初始化,为了保证线程安全,使当前线程先让出 CPU 时间片;
14 // - 如果 sc > 0,则说明没有其它线程执行初始化,当前线程可以执行初始化操作;
15 if ((sc = sizeCtl) < 0) {
16 Thread.yield();
17 }
18 // 使用 CAS 方法,将当前对象中的 SIZECTL 属性值与 sc 进行比较:
19 // - 如果值相等,则将 SIZECTL 的值更新为 -1,然后执行 if 中的逻辑;
20 // - 如果值不相等,不执行 if 中的逻辑;
21 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
22 try {
23 // 设置变量 tab = table,然后判断 table 是否为空,是则执行初始化操作。
24 if ((tab = table) == null || tab.length == 0) {
25 // 如果初始化容量了,则设置为 sc,否则设置为默认容量 16
26 int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
27 @SuppressWarnings("unchecked")
28 // 初始化 table 数组
29 Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
30 table = tab = nt;
31 // 计算下次扩容的大小,实际就是当前容量的 0.75 倍,这里使用了右移来计算
32 sc = n - (n >>> 2);
33 }
34 } finally {
35 // 设置 sizeCtl 为 sc,如果默认是 16 的话,则 sc 为 16*0.75=12
36 sizeCtl = sc;
37 }
38 break;
39 }
40 }
41 // 返回初始后的表
42 return tab;
43}
tabAt(Node<K,V>[] tab, int i)
1/**
2 * 获取 tab 数组中下标为 i 的元素。
3 *
4 * 这里之所以使用 getObjectVolatile() 方法获取数组中指定位置的元素,这是因为该方法属于 volatile 类型方法,
5 * 可以保证数组中数据的可见性,其它线程对当前数组进行修改后,当前线程可以马上观察到最新的改动。
6 *
7 * @param tab 待操作的数组
8 * @param i 数组的下标位置
9 * @return 数组指定下标位置的元素
10 */
11static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
12 // ABASE 表示数组地址,(long)i << ASHIFT 表示偏移量
13 return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
14}
casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v)
1/**
2 * 将 tab 数组中下标为 i 的元素与预期值 c 进行比较,如果相同则将 i 位置元素更新为 v,否则不更新。
3 *
4 * 使用 compareAndSwapObject 方法进行更新,可以保证多线程环境下的操作的线程安全。
5 *
6 * @param tab 待操作的数组
7 * @param i 数组的下标位置
8 * @param c 预期值
9 * @param v 如果符合预期进行更新的值
10 * @return 如果 tab 数组中下标为 i 的元素与预期值 c 相同,则返回 ture,否则返回 false
11 */
12static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
13 return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
14}
4.3 插入方法 putVal(K key, V value)
put(K key, V value)
1/**
2 * 将指定的键值对插入到 ConcurrentHashMap 集合中。 (不允许插入null键和null值)
3 *
4 * @param key 待插入的键
5 * @param value 待插入的值
6 * @return 返回与 Key 关联的旧值,分两种情况:
7 * - 如果集合中不存在对应的 key,即插入一个新的 key,则返回 null;
8 * - 如果集合中已经存在一个相同的 key,则新 value 覆盖旧 value,然后返回旧 value;
9 * @throws NullPointerException 空指针异常
10 */
11public V put(K key, V value) {
12 return putVal(key, value, false);
13}
putVal(K key, V value, boolean onlyIfAbsent)
1/**
2 * 实现 Map.put() 和相关方法。
3 *
4 * @param key 待插入的键
5 * @param value 待插入的值
6 * @param onlyIfAbsent 如果为 true 则不更改现有的值
7 * @return 如果插入的 Key 已经存在,则返回已存在 Key 的节点值,否则返回 null
8 */
9final V putVal(K key, V value, boolean onlyIfAbsent) {
10 // 如果插入的 key 或者 value 为 null,则抛出空指针异常
11 if (key == null || value == null) {
12 throw new NullPointerException();
13 }
14 // 计算 hash 值
15 int hash = spread(key.hashCode());
16 // 创建变量 binCount,使用改变量记录,当数组下标位置关联的为链表或红黑树时:
17 // - 如果是链表,则设置 binCount 值为链表的节点数量,后续用于判断是否需要将链表转换为红黑树;
18 // - 如果是红黑树,则设置 binCount 值为固定值 2,表示在红黑树中插入或者替换了值,用于后续判断是否需要返回旧值;
19 int binCount = 0;
20
21 // 死循环执行循环代码块中的逻辑 (自旋操作)
22 for (Node<K,V>[] tab = table;;) {
23 // f: 记录待插入 key 经计算找到在 table 数组中的下标位置节点的变量
24 // n: 记录 table 数组长度的变量
25 // i: 记录待插入 key 经计算找到在 table 数组中的下标位置的变量
26 // fh: 记录 table 数组中的下标位置节点 hash 值的变量
27 Node<K,V> f; int n, i, fh;
28
29 // --- 1、判断 table 数组是否初始化 ---
30 // 判断 table 数组是否为空,如果是则调用 initTable() 方法进行初始化操作
31 if (tab == null || (n = tab.length) == 0) {
32 tab = initTable();
33 }
34 // --- 2、判断 table 数组下标位置是否哈希冲突 ---
35 // 调用 tabAt() 方法获取插入 key 在 table 数组中下标位置的节点,并赋给变量 f,然后判断节点是否为 null:
36 // - 如果为 null,则说明当前数组下标位置为空,则执行 else if 条件中的代码逻辑;
37 // - 如果不为 null,则说明当前数组下标位置已经存在数据,发生了哈希冲突,不执行 else if 条件中的代码逻辑;
38 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
39 // 如果数组下标位置的节点为 null,则使用 CAS 方法,判断当前数组下标位置是否为 null:
40 // - 如果当前数组下标位置为 null,则将新创建的键值对 Node 节点加入到该位置;
41 // - 如果当前数组下标位置不为 null,则不进行任何操作;
42 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) {
43 break; // no lock when adding to empty bin
44 }
45 }
46 // --- 3、判断当前集合是否正在执行扩容 ---
47 // 获取数组下标位置节点的 hash 值并赋给变量 fh,然后判断 hash 值是否为 MOVED(-1):
48 // - 如果 hash 值为 -1,则说明有其它线程正在对当前集合进行扩容,当前桶位置的节点为 ForwardingNode 转移节点,
49 // 这时当前线程就会执行 helpTransfer() 方法协助其它线程进行扩容操作,然后将插入的数据加入扩容后的新数组中;
50 // - 如果 hash 值不为 -1,则说明当前结合没有进行扩容,不执行 if 中的代码逻辑;
51 else if ((fh = f.hash) == MOVED) {
52 tab = helpTransfer(tab, f);
53 }
54 // --- 4、将插入的键值对加入到链表或者红黑树中 ---
55 else {
56 // 记录找到的节点的原先 value 值的变量
57 V oldVal = null;
58 // 使用 synchronized 进行加锁,并以当前数组下标位置节点 f 作为锁对象
59 synchronized (f) {
60 // 再次判断当前数组下标位置的节点是否为 f,只所以需要再次判断,这是因为在添加元素的时候,
61 // 有可能同时又有其它线程在移除元素,这种情况需要自旋操作来重新插入。
62 if (tabAt(tab, i) == f) {
63 // 判断数组下标位置节点的 hash 值是否大于等于 0:
64 // - 如果 hash >= 0,则说明当前数组下标位置关联的是链表;
65 // - 如果 hash < 0,则说明当前数组下标位置关联的是红黑树;
66 if (fh >= 0) {
67 // 遍历链表,寻找是否存在与插入 key 相同的节点,并且使 binCount 值 +1
68 binCount = 1;
69 for (Node<K,V> e = f;; ++binCount) {
70 // 记录遍历过程中当前节点的 key 的变量
71 K ek;
72 // 如果找到插入 key 的节点,则记录当前节点的值赋给变量 oldVal,然后将插入的新 value 替换掉旧值,
73 // 最后执行 break 指令终止继续遍历链表。
74 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
75 oldVal = e.val;
76 if (!onlyIfAbsent) {
77 e.val = value;
78 }
79 break;
80 }
81 // 记录前驱节点
82 Node<K,V> pred = e;
83 // 记录后继节点,并判断当前遍历到的位置是否到了链表末尾节点,如果是就创建新的节点加入到链表末尾
84 if ((e = e.next) == null) {
85 pred.next = new Node<K,V>(hash, key, value, null);
86 break;
87 }
88 }
89 }
90 // 如果数组下标位置的节点 f 为红黑树节点,则将键值对数据加入到红黑树中
91 else if (f instanceof TreeBin) {
92 // 记录调用 putTreeVal() 方法的返回结果
93 Node<K,V> p;
94 // 设置 binCount 为 2
95 binCount = 2;
96 // 调用 putTreeVal() 方法将键值对数据加入到红黑树中,并且判断方法返回结果:
97 // - 如果方法返回结果为 null,则说明执行了添加操作,不用记录老值;
98 // - 如果方法返回结果不为 null,则说明执行了替换操作,需要记录老值赋给变量 oldVal,
99 // 然后将插入的新 value 替换掉旧值;
100 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
101 oldVal = p.val;
102 if (!onlyIfAbsent) {
103 p.val = value;
104 }
105 }
106 }
107 }
108 }
109 // 如果 binCount 不为 0 则说明在链表或者红黑树中进行了插入或替换操作,则执行 if 中的代码逻辑
110 if (binCount != 0) {
111 // 判断变量 binCount 是否大于等于 TREEIFY_THRESHOLD(8),如果是则尝试将链表转换为红黑树,不过:
112 // - 如果当前集合大小 < 64 树化最小限制容量 MIN_TREEIFY_CAPACITY(64),则不会转换为红黑树,
113 // 只会对当前集合进行一次扩容操作;
114 // - 如果当前集合大小 ≥ 树化最小限制容量 IN_TREEIFY_CAPACITY(64),则就将链表转换为红黑树;
115 if (binCount >= TREEIFY_THRESHOLD) {
116 treeifyBin(tab, i);
117 }
118 // 判断老值是否不为空,是则返回老值
119 if (oldVal != null) {
120 return oldVal;
121 }
122 break;
123 }
124 }
125 }
126 // 执行到这里说明进行了添加操作,而没有进行替换操作,添加数据就使结合大小 +1,然后判断是否达到了库容阈值,是就进行扩容
127 addCount(1L, binCount);
128 return null;
129}
4.4 删除方法 remove(Object key)
remove(Object key)
1/**
2 * 从集合中删除指定 key 对应的键值对数据
3 *
4 * @param key 待删除的键值对 key
5 * @return 判断集合中存在指定的 key 对应的键值对数据,不存在则返回 null,存在则返回键值对的 value
6 * @throws NullPointerException 空指针异常
7 */
8public V remove(Object key) {
9 return replaceNode(key, null, null);
10}
replaceNode(Object key, V value, Object cv)
1/**
2 * 替换/删除方法
3 * - 如果传入参数 value 为空,则执行删除操作;
4 * - 如果传入参数 value 不为空,则执行替换操作,将 cv 替换为 value;
5 *
6 * @param key 键值对 key
7 * @param value 键值对 value
8 * @param cv 待替换的旧值
9 * @return 如果执行删除操作则返回 null,如果执行替换操作则返回旧值
10 */
11final V replaceNode(Object key, V value, Object cv) {
12 int hash = spread(key.hashCode());
13 for ( Node<K,V>[] tab = table;;) {
14 Node<K,V> f; // 记录数组下标位置节点的变量
15 int n; // 记录数组 table 长度的变量
16 int i; // 记录数组下标位置索引的变量
17 int fh; // 记录数组下标位置哈希值的变量
18
19 // 如果当前数组 table 为空,或者数组下标位置为空,则表示数据已经被删除,直接结束循环
20 if (tab == null || (n = tab.length) == 0 || (f = tabAt(tab, i = (n - 1) & hash)) == null) {
21 break;
22 }
23 // 获取数组下标位置节点的 hash 值并赋给变量 fh,然后判断 hash 值是否为 MOVED(-1):
24 // - 如果 hash 值为 -1,则说明有其它线程正在对当前集合进行扩容,当前桶位置的节点为 ForwardingNode 转移节点,
25 // 这时当前线程就会执行 helpTransfer() 方法协助其它线程进行扩容操作,然后将插入的数据加入扩容后的新数组中;
26 // - 如果 hash 值不为 -1,则说明当前结合没有进行扩容,不执行 if 中的代码逻辑;
27 else if ((fh = f.hash) == MOVED) {
28 tab = helpTransfer(tab, f);
29 }
30 else {
31 V oldVal = null; // 记录找到的节点的原先 value 值的变量
32 boolean validated = false; // 记录是否处理过当前数组下标位置关联的链表或者红黑树
33
34 // 使用 synchronized 进行加锁,并以当前数组下标位置节点 f 作为锁对象
35 synchronized (f) {
36 // 再次判断当前数组下标位置的节点是否为 f,只所以需要再次判断,这是因为在添加元素的时候,
37 // 有可能同时又有其它线程在移除元素,这种情况需要自旋操作来重新操作。
38 if (tabAt(tab, i) == f) {
39 // 判断数组下标位置节点的 hash 值是否大于等于 0:
40 // - 如果 hash >= 0,则说明当前数组下标位置关联的是链表;
41 // - 如果 hash < 0,则说明当前数组下标位置关联的是红黑树;
42 if (fh >= 0) {
43 // 标记为 true,表示处理过
44 validated = true;
45 // 从链表头结点开始遍历链表
46 for (Node<K,V> e = f, pred = null;;) {
47 // 记录遍历过程中当前节点的 key 的变量
48 K ek;
49 // 如果找到指定 key 的节点,则记录当前节点的值赋给变量 oldVal,然后将插入的新 value 替换掉旧值,
50 // 最后执行 break 指令终止继续遍历链表。
51 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
52 // 记录当前节点的 value 值
53 V ev = e.val;
54 // 判断是否传入 cv:
55 // - 如果传入的旧值 cv 为 null 则执行下面替换/删除逻辑;
56 // - 如果当前节点 value 和传入旧值 cv 相同,则执行下面替换/删除逻辑;
57 if (cv == null || cv == ev || (ev != null && cv.equals(ev))) {
58 // 将旧值赋给变量 oldVal
59 oldVal = ev;
60 // 判断 value 是否为空:
61 // - 如果 value 不为空,则说明要执行替换操作;
62 // - 如果 value 为空,则说明要执行删除操作;
63 if (value != null) {
64 // 将旧值替换为 value
65 e.val = value;
66 }
67 // 判断将当前节点是否有前驱节点:
68 // - 如果有前驱节点,则设置 pred.next = e.next 来进行删除当前节点;
69 // - 如果没有前驱节点,则说明当前节点是头结点,设置 setTabAt(tab, i, e.next) 直接删除当前节点;
70 else if (pred != null) {
71 pred.next = e.next;
72 }
73 else {
74 setTabAt(tab, i, e.next);
75 }
76 }
77 break;
78 }
79 // 设置前驱节点为当前节点
80 pred = e;
81 // 将后继节点赋给变量 e,然后判断其是否为 null,如果是则说明已经遍历到尾节点,直接结束循环
82 if ((e = e.next) == null) {
83 break;
84 }
85 }
86 }
87 // 判断当前节点是否为树节点
88 else if (f instanceof TreeBin) {
89 // 标记为 true,表示处理过
90 validated = true;
91 // 将节点 Node 强转为 TreeBin
92 TreeBin<K,V> t = (TreeBin<K,V>)f;
93 TreeNode<K,V> r; // 记录树 root 节点的变量
94 TreeNode<K,V> p; // 记录接收 findTreeNode 方法返回结果的变量
95 // 将 root 节点赋给变量 r,然后调用 findTreeNode 方法遍历树查找指定 key 节点:
96 // - 如果调用 findTreeNode() 方法返回找到的指定 key 的树节点,则执行下面逻辑;
97 // - 如果调用 findTreeNode() 方法返回 null,则没有找到,不执行下面逻辑;
98 if ((r = t.root) != null && (p = r.findTreeNode(hash, key, null)) != null) {
99 // 将找到的树节点赋给变量 pv
100 V pv = p.val;
101 // 判断是否传入 cv:
102 // - 如果传入的旧值 cv 为 null 则执行下面替换/删除逻辑;
103 // - 如果当前节点 value 和传入旧值 cv 相同,则执行下面替换/删除逻辑;
104 if (cv == null || cv == pv || (pv != null && cv.equals(pv))) {
105 // 将旧值赋给变量 oldVal
106 oldVal = pv;
107 // 判断 value 是否为空:
108 // - 如果 value 不为空,则说明要执行替换操作;
109 // - 如果 value 为空,则说明要执行删除操作;
110 if (value != null) {
111 p.val = value;
112 }
113 // 执行到这里说明 value 为空,执行删除操作,调用 t.removeTreeNode(p) 方法删除当前节点,
114 // 然后判断方法返回结果:
115 // - 如果方法返回 true,则表示删除节点后树的元素个数较少,则将红黑树转换为链表;;
116 // - 如果方法返回 false,则表示删除节点后树的元素个数依旧很多,则不将红黑树转换为链表;
117 else if (t.removeTreeNode(p)) {
118 setTabAt(tab, i, untreeify(t.first));
119 }
120 }
121 }
122 }
123 }
124 }
125 // 如果处理过,不管有没有找到元素都返回
126 if (validated) {
127 // 判断变量 oldVal 是否为空:
128 // - 如果不为 null 则说明存在旧值,返回结果将旧值返回;
129 // - 如果为 null 则说明不存在旧值,返回结果返回 null;
130 if (oldVal != null) {
131 // 判断 value 是否为空:
132 // - 如果 value 不为空,则说明要执行替换操作;
133 // - 如果 value 为空,则说明要执行删除操作,这里执行 addCount 方法减小集合大小;
134 if (value == null) {
135 addCount(-1L, -1);
136 }
137 return oldVal;
138 }
139 break;
140 }
141 }
142 }
143 return null;
144}
4.5 协助扩容方法 helpTransfer(Node<K,V>[] tab, Node<K,V> f)
1 /**
2 * 协助扩容
3 *
4 * @param tab 待操作的数组
5 * @param f 需要查找的节点
6 * @return 如果扩容结束则返回当前 tabel 数组,否则返回新的 table 数组
7 */
8final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
9 Node<K,V>[] nextTab; // 下一个节点
10 int sc; // 记录 sizeCtl 的变量
11
12 // 如果旧 table 不为空,并且要查找的节点是转移节点,则判断新 table 是否为空:
13 // - 如果新 table 为空,则说明扩容已经结束;
14 // - 如果新 table 不为空,则说明扩容正在进行,当前线程协助集合进行扩容操作;
15 if (tab != null && (f instanceof ForwardingNode) && (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
16 // 根据数组长度得到一个扩容戳
17 int rs = resizeStamp(tab.length);
18 // 循环判断是否扩容完成
19 while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) {
20 // 判断你是否符合下面条件,如果符合则说明扩容结束,则直接退出循环,判断条件如下:
21 // - sc >>> RESIZE_STAMP_SHIFT != rs 表示同一轮扩容中 sc 无符号右移比较高位和 rs 的值应该是相等的,
22 // 如果不相等说明扩容结束;
23 // - rs + 1 表示扩容结束;
24 // - rs + MAX_RESIZERS 表示扩容线程数达到最大扩容线程数;
25 // - transferIndex <= 0 表示所有的 Node 都已经分配了线程;
26 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || transferIndex <= 0) {
27 break;
28 }
29 // 每增加一个线程来协助迁移就使 sizeCtl 值 +1, 然后当前线程协助扩容
30 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
31 transfer(tab, nextTab);
32 break;
33 }
34 }
35 // 返回新的 table 数组
36 return nextTab;
37 }
38 // 返回当前 table 数组
39 return table;
40}
4.6 链表转红黑树方法 treeifyBin(Node<K,V>[] tab, int index)
1/**
2 * 链表转换为红黑树
3 *
4 * @param tab 数组 table
5 * @param index 数组索引位置
6 */
7private final void treeifyBin(Node<K,V>[] tab, int index) {
8 Node<K,V> b; // 记录数组指定索引位置节点的变量
9 int n, sc; // 记录数组长度
10
11 // 如果数组 table 不为空,则执行 if 中的代码逻辑
12 if (tab != null) {
13 // 判断当前数组长度是否达到了转换为红黑树要求的最小容量:
14 // - 如果当前数组长度 < 转换为红黑树要求的最小容量,则不转换为红黑树,只对 table 数组进行扩容;
15 // - 如果当前数组长度 ≥ 转换为红黑树要求的最小容量,则将数组指定索引位置关联的链表转换为红黑树;
16 if ((n = tab.length) < MIN_TREEIFY_CAPACITY) {
17 tryPresize(n << 1);
18 }
19 else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
20 // 使用同步锁,并且以数组索引下标位置的节点作为锁对象
21 synchronized (b) {
22 // 再次验证数组下标索引下标位置的节点是否和变量 b 记录的节点相同,是则执行链表转换为红黑树的逻辑
23 if (tabAt(tab, index) == b) {
24 TreeNode<K,V> hd = null, tl = null;
25 for (Node<K,V> e = b; e != null; e = e.next) {
26 TreeNode<K,V> p = new TreeNode<K,V>(e.hash, e.key, e.val, null, null);
27 if ((p.prev = tl) == null) {
28 hd = p;
29 } else {
30 tl.next = p;
31 }
32 tl = p;
33 }
34 setTabAt(tab, index, new TreeBin<K,V>(hd));
35 }
36 }
37 }
38 }
39}
4.7 红黑树转链表方法 untreeify(Node<K,V> b)
1/**
2 * 取消树化,将红黑树转换为链表
3 *
4 * @param b 待转换的红黑树节点
5 * @return 链表
6 */
7static <K,V> Node<K,V> untreeify(Node<K,V> b) {
8 Node<K,V> hd = null; // 记录链表头节点的变量
9 Node<K,V> tl = null; // 记录链表尾节点的变量
10
11 // 遍历 Node 节点
12 for (Node<K,V> q = b; q != null; q = q.next) {
13 // 根据已有的节点创建的数据,创建新节点
14 Node<K,V> p = new Node<K,V>(q.hash, q.key, q.val, null);
15 // 如果为遍历的第一次,则记录链表的头结点
16 if (tl == null) {
17 hd = p;
18 }
19 // 将上一个节点的下一个节点指向当前这个节点
20 else {
21 tl.next = p;
22 }
23 // 将 tl 节点指向 p
24 tl = p;
25 }
26 // 返回链表的头结点
27 return hd;
28}
4.8 尝试扩容方法 tryPresize(int size)
1/**
2 * 尝试调整当前集合数组 table 的大小,以容纳给定数量的元素。
3 *
4 * @param size 带插入集合的大小
5 */
6private final void tryPresize(int size) {
7 // 判断集合大小 size 是否大于等于 2^29:
8 // - 如果是,则设置变量 c 为集合大小为集合最大容量 MAXIMUM_CAPACITY;
9 // - 如果不是,则设置变量 c 为调用 tableSizeFor 方法的结果;
10 int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(size + (size >>> 1) + 1);
11 int sc; // 记录 sizeCtl 的变量
12
13 // 循环执行,并且判断 sizeCtl >= 0:
14 // - 如果 sizeCtl < 0 则表示其它线程正在对当前集合进行初始化或扩容;
15 // - 如果 sizeCtl ≥ 0 则表示集合已经扩容完成,则执行下面 while 代码块中的逻辑;
16 while ((sc = sizeCtl) >= 0) {
17 Node<K,V>[] tab = table; // 记录数组 table 的变量
18 int n; // 记录数组 table 长度的变量
19
20 // 如果数组 table 当前为空,则说明数组 table 还没有初始化,先执行初始化操作
21 if (tab == null || (n = tab.length) == 0) {
22 n = (sc > c) ? sc : c;
23 if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
24 try {
25 if (table == tab) {
26 @SuppressWarnings("unchecked")
27 Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
28 table = nt;
29 // 相当于执行 n*0.75,即计算扩容阈值
30 sc = n - (n >>> 2);
31 }
32 } finally {
33 // 设置 sizeCtl 值为 sc 值
34 sizeCtl = sc;
35 }
36 }
37 }
38 // 数组容量已经最大或足够了,不需要扩容
39 // size 大小 ≤ 扩容阈值,或者 ≥ 最大容量限制值时,不需要进行扩容
40 else if (c <= sc || n >= MAXIMUM_CAPACITY) {
41 break;
42 }
43 // 验证 tab 记录的变量是否为 table 数组,如果是就进行扩容操作
44 else if (tab == table) {
45 // 根据数组长度得到一个扩容戳
46 // (注: 每次扩容时都会生成一个类似时间戳的校验标记,上一次扩容和下一次扩容都不一样,这个值由当前数组的长度决定)
47 int rs = resizeStamp(n);
48 // 判断变量 sizeCtl 的值是否小于 0:
49 // - 如果 sizeCtl ≥ 0 则表示集合已经扩容完成;
50 // - 如果 sizeCtl < 0 则表示其它线程正在对当前集合进行初始化或扩容,则当前线程进行协助扩容;
51 // (注: 扩容时 sizeCtl 高 16 位表示扩容戳(校验标记),低 16 位表示(正在参与扩容的线程数+1))
52 if (sc < 0) {
53 Node<K,V>[] nt;
54 // 判断你是否符合下面条件,如果符合则说明扩容结束,则直接退出循环,判断条件如下:
55 // - sc >>> RESIZE_STAMP_SHIFT != rs 表示同一轮扩容中 sc 无符号右移比较高位和 rs 的值应该是相等的,
56 // 如果不相等说明扩容结束;
57 // - rs + 1 表示扩容结束;
58 // - rs + MAX_RESIZERS 表示扩容线程数达到最大扩容线程数;
59 // - (nt = nextTable) == null 表示 nextTable 还没有初始化或者正在进行初始化;
60 // - transferIndex <= 0 表示所有的 Node 都已经分配了线程;
61 if ((sc >>> RESIZE_STAMP_SHIFT) != rs
62 || sc == rs + 1
63 || sc == rs + MAX_RESIZERS
64 || (nt = nextTable) == null
65 || transferIndex <= 0) {
66 break;
67 }
68 // 每增加一个线程来协助迁移,就使用 CAS 方法使 sizeCtl 值 +1, 然后当前线程协助扩容
69 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
70 transfer(tab, nt);
71 }
72 }
73 // 如果当前没有在扩容,那么 rs 肯定是一个正数
74 // 通过 rs<<RESIZE_STAMP_SHIFT 将 sc 设置为一个负数,+2 表示有一个线程在执行扩容
75 else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) {
76 transfer(tab, null);
77 }
78 }
79 }
80}
4.9 扩容方法 transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)
1/**
2 * 扩容方法
3 *
4 * @param tab 当前 table
5 * @param nextTab 扩容后的 table
6 */
7private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
8 // 记录当前数组 tabel 的长度
9 int n = tab.length, stride;
10 // 如果处 CPU 数量大于 1,则通过当前 table 中 Node 总量和 CPU 的数量来计算步长,即每个线程所允许处理的桶的数量
11 // 注:
12 // - NCPU 表示可以使用的 CPU 的数量;
13 // - MIN_TRANSFER_STRIDE 表示并发扩容时每个线程最少处理16个桶;
14 if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) {
15 stride = MIN_TRANSFER_STRIDE;
16 }
17 // 如果 nextTab 为 null,则说明是第一个线程进行转移,执行下面 if 中的逻辑。
18 if (nextTab == null) {
19 try {
20 // 创建一个容量为当前数组容量两倍的新数组,并赋给变量 nextTab,
21 @SuppressWarnings("unchecked")
22 Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
23 nextTab = nt;
24 } catch (Throwable ex) {
25 // 如果执行过程中发生异常,则设置 sizeCtl = Integer.MAX_VALUE,然后直接退出不转移了
26 sizeCtl = Integer.MAX_VALUE;
27 return;
28 }
29 // 将新数组赋值给 nextTable,此时数组为一个没有数据的空数组
30 nextTable = nextTab;
31 // 使变量 transferIndex 记录当前数组的长度 n,表示转移开始的索引值
32 transferIndex = n;
33 }
34 // 将新数组的长度赋给变量 nextn
35 int nextn = nextTab.length;
36 // 变量 fwd 是于来标识某个桶处正在转移,其存储了新数组
37 ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
38 // 变量 advance 标记用于确认是否需要在区间内继续往前前进处理
39 boolean advance = true;
40 // 变量 finishing 用于标记是否将所有的区间都迁移完成了
41 boolean finishing = false;
42 // 将 i 和 bound 的值初始化为 0,然后进行自旋,直到数据转移成功后再退出循环
43 for (int i = 0, bound = 0;;) {
44 Node<K,V> f; // 记录当前数组指定位置节点的变量
45 int fh; // 记录当前数组指定位置节点哈希值的变量
46 // 控制 --i,遍历原数组中的节点
47 while (advance) {
48 int nextIndex, nextBound;
49 // 某个线程的第二次循环时候才会走到这里来,说明 i 还在该线程需要转移的区间内,即 [bound <--> i <--> transferIndex)
50 if (--i >= bound || finishing) {
51 advance = false;
52 }
53 // transferIndex ≤ 0 说明已经到最前面了,当前 table 中所有数组元素都已经有其他线程负责扩容
54 else if ((nextIndex = transferIndex) <= 0) {
55 i = -1;
56 advance = false;
57 }
58 // 走到这里说明 i < bound 且 transferIndex > 0 (即整个当前 table 数组中的桶的转移还没处理完成)
59 // 通过 CAS 继续认领一段区间进行转移,计算出当前线程所需要处理的区间,nextBound 就是下一个线程的下边界,
60 // 也是当线程的上边界
61 else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
62 nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
63 bound = nextBound;
64 i = nextIndex - 1;
65 advance = false;
66 }
67 }
68 // 如果 i < 0 或者 i >= n 说明已经超出了所需要处理的区间了
69 if (i < 0 || i >= n || i + n >= nextn) {
70 int sc;
71 // 判断遍历 finishing 的值:
72 // - 如果 finishing 为 true,则说明所有的桶都处理完了,执行下面 if 中的代码逻辑;
73 // - 如果 finishing 为 false,则说明害有桶没有处理完成,不执行 if 中的代码逻辑;
74 if (finishing) {
75 // 将 nextTable 置为空,并将新的数组赋值给变量 table
76 nextTable = null;
77 table = nextTab;
78 // 设置新的阈值,新数组的长度为 2n,所以阈值应该为 0.75 * 2n,即 2n - 0.5n
79 sizeCtl = (n << 1) - (n >>> 1);
80 // 跳出循环
81 return;
82 }
83 // CAS 更扩容阈值,在这里面 sizeCtl 值减 -1,说明新加入一个线程参与到扩容操作
84 if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
85 if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) {
86 return;
87 }
88 // 设置变量 finishing 和 advance 状态为 true
89 finishing = advance = true;
90 i = n;
91 }
92 }
93 // 如果索引为 i 的桶处为 null,则通过 CAS 方法将 fwd 转移节点放在原数组 i 索引的下标位置,
94 // 然后将 advance 设置为 true,然后退出循环继续往前
95 else if ((f = tabAt(tab, i)) == null) {
96 advance = casTabAt(tab, i, null, fwd);
97 }
98 // 获取当前数组 table 中 i 索引下标位置节点的 hash 值并赋给变量 fh,然后判断 hash 值是否为 MOVED(-1):
99 // - 如果 hash 值为 -1,则说明有其它线程正在对当前集合进行扩容,当前桶位置的节点为 ForwardingNode 转移节点,
100 // 这时就会将 advance 设置为 true,然后退出循环继续往前;
101 // - 如果 hash 值不为 -1,则说明当前结合没有进行扩容,不执行 if 中的代码逻辑;
102 else if ((fh = f.hash) == MOVED) {
103 advance = true;
104 } else {
105 // 使用 synchronized 进行加锁,并以当前数组下标位置节点 f 作为锁对象
106 synchronized (f) {
107 // 再次判断当前数组下标位置的节点是否为 f,只所以需要再次判断,这是因为在添加元素的时候,
108 // 有可能同时又有其它线程在移除元素,这种情况需要自旋操作来重新插入。
109 if (tabAt(tab, i) == f) {
110 // 判断数组下标位置节点的 hash 值是否大于等于 0:
111 // - 如果 hash >= 0,则说明当前数组下标位置关联的是链表;
112 // - 如果 hash < 0,则说明当前数组下标位置关联的是红黑树;
113 if (fh >= 0) {
114 // 遍历链表,找到第一个后面全部为相同的 runBit 的节点
115 int runBit = fh & n;
116 Node<K,V> lastRun = f;
117 for (Node<K,V> p = f.next; p != null; p = p.next) {
118 int b = p.hash & n;
119 if (b != runBit) {
120 runBit = b;
121 lastRun = p;
122 }
123 }
124 // 如果 runBit == 0,则说明这些节点还要放在相同 index 的桶里,赋给变量 ln
125 if (runBit == 0) {
126 ln = lastRun;
127 hn = null;
128 }
129 // 否则 runBit == n,则说明这些节点还要放在相同 index + n 的桶里,赋给变量 hn
130 else {
131 hn = lastRun;
132 ln = null;
133 }
134 // 如果节点只有单个数据则直接拷贝,如果是链表则对链表进行遍历拷贝
135 for (Node<K,V> p = f; p != lastRun; p = p.next) {
136 int ph = p.hash; K pk = p.key; V pv = p.val;
137 if ((ph & n) == 0) {
138 ln = new Node<K,V>(ph, pk, pv, ln);
139 } else {
140 hn = new Node<K,V>(ph, pk, pv, hn);
141 }
142 }
143 // 将节点 ln 插入新数组 table 的 i 处
144 setTabAt(nextTab, i, ln);
145 // 将节点 hn 插入新数组 table 的 i+n 处
146 setTabAt(nextTab, i + n, hn);
147 // 在原先 table 数组 i 位置设置 ForwardingNode 节点,表示该节点已经处理过了
148 // 将原数组 table 的索引 i 位置的节点设置为 fwd 节点
149 setTabAt(tab, i, fwd);
150 // 更新 advance 为 true,准备下次 for 循环
151 advance = true;
152 }
153 // 如果是 TreeBin,则按照红黑树进行处理,处理逻辑与上面一致
154 else if (f instanceof TreeBin) {
155 TreeBin<K,V> t = (TreeBin<K,V>)f;
156 TreeNode<K,V> lo = null, loTail = null;
157 TreeNode<K,V> hi = null, hiTail = null;
158 int lc = 0, hc = 0;
159 for (Node<K,V> e = t.first; e != null; e = e.next) {
160 int h = e.hash;
161 TreeNode<K,V> p = new TreeNode<K,V>(h, e.key, e.val, null, null);
162 if ((h & n) == 0) {
163 if ((p.prev = loTail) == null) {
164 lo = p;
165 } else {
166 loTail.next = p;
167 }
168 loTail = p;
169 ++lc;
170 }
171 else {
172 if ((p.prev = hiTail) == null) {
173 hi = p;
174 } else {
175 hiTail.next = p;
176 }
177 hiTail = p;
178 ++hc;
179 }
180 }
181 // 扩容后树节点个数比较少的话,将红黑树转换为链表
182 ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : (hc != 0) ? new TreeBin<K,V>(lo) : t;
183 hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : (lc != 0) ? new TreeBin<K,V>(hi) : t;
184 setTabAt(nextTab, i, ln);
185 setTabAt(nextTab, i + n, hn);
186 setTabAt(tab, i, fwd);
187 advance = true;
188 }
189 }
190 }
191 }
192 }
193}
--- END ---
!版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。