Map (一):HashMap 及 ConcurrentHashMap 解析

写在前面

本文中的内容并非完全原创,主要来源见文末的参考链接,本人仅作整理工作,用以记录自己的学习过程,由于个人水平有限,故部分内容可能会出现错误,还请包涵

思维导图

HashMap

由数组 + 链表 + 红黑树组成

HashMap 中 Node 的 key、value 值均可为null

在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树

Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode

  • 链表中的 Node 节点实现了 Map.Entry<K, V> 接口,而红黑树中的 TreeNode 继承自 LinkedHashMap.Entry<K,V> 类,而 LinkedHashMap.Entry<K,V> 类又继承自 HashMap.Node<K,V> 类。即 TreeNode 是 Node 的子类

HashMap 中重要的属性

transient Node<K,V>[] table : 存储 Map 键值对元素的数组

capacity() : 当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍

loadFactor : 负载因子,默认为 0.75

threshold : 扩容的阈值,等于 capacity * loadFactor

hash

1
2
3
4
5
6
7
static final int hash(Object key) {

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

为何如此计算 hash 值暂不清楚

put

Java7 是在链表头部插入新节点,Java8 在链表尾部插入新节点

Java7 是先扩容后插入新值的,Java8 先插值再扩容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
public V put(K key, V value) {

return putVal(hash(key), key, value, false, true);

}

// 第三个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作
// 第四个参数 evict 我们这里不关心
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {

Node<K,V>[] tab; Node<K,V> p; int n, i;

// 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度
// 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量
if ((tab = table) == null || (n = tab.length) == 0)

n = (tab = resize()).length;

// 找到具体的数组下标,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
if ((p = tab[i = (n - 1) & hash]) == null)

tab[i] = newNode(hash, key, value, null);

else {// 数组该位置有数据
Node<K,V> e; K k;

// 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))

e = p;

// 如果该节点是代表红黑树的节点,调用红黑树的插值方法,本文不展开说红黑树
else if (p instanceof TreeNode)

e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

else {

// 到这里,说明数组该位置上是一个链表
for (int binCount = 0; ; ++binCount) {

// 插入到链表的最后面(Java7 是插入到链表的最前面)
if ((e = p.next) == null) {

p.next = newNode(hash, key, value, null);

// TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 9 个
// 会触发下面的 treeifyBin,也就是将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

treeifyBin(tab, hash);

break;

}

// 如果在该链表中找到了"相等"的 key(== 或 equals)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))

// 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node
break;

p = e;

}

}

// e != null 说明存在旧值的 key 与要插入的 key "相等"
// 对于我们分析的 put 操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值
if (e != null) {

V oldValue = e.value;

if (!onlyIfAbsent || oldValue == null)
e.value = value;

afterNodeAccess(e);

return oldValue;

}

}

++modCount;

// 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容
// 由此可以看出 JDK 1.8 中 HashMap 是先插入元素再判断是否进行扩容
if (++size > threshold)

resize();

afterNodeInsertion(evict);

return null;

}

数组扩容 resize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
final Node<K,V>[] resize() {

Node<K,V>[] oldTab = table;

int oldCap = (oldTab == null) ? 0 : oldTab.length;

int oldThr = threshold;

int newCap, newThr = 0;

if (oldCap > 0) { // 对应数组扩容

if (oldCap >= MAXIMUM_CAPACITY) {

threshold = Integer.MAX_VALUE;

return oldTab;

}

// 将数组大小扩大一倍

else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

oldCap >= DEFAULT_INITIAL_CAPACITY)

// 将阈值扩大一倍

newThr = oldThr << 1; // double threshold

}

// 这里解释了为什么 HashMap 的构造函数中将 initialCapacity 赋给 this.threshold
else if (oldThr > 0) // 对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候

newCap = oldThr;

else {// 对应使用 new HashMap() 初始化后,第一次 put 的时候

newCap = DEFAULT_INITIAL_CAPACITY;

newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

}


// 对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候
// 因为之前对应的步骤没有为 newThr 赋值
if (newThr == 0) {

float ft = (float)newCap * loadFactor;

newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

(int)ft : Integer.MAX_VALUE);

}

// 从这里可以看出构造函数里给 this.threshold 赋的 initialCapacity 值并不是真正的 threshold 值
threshold = newThr;

// 用新的数组大小初始化新的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

table = newTab; // 如果是初始化数组,到这里就结束了,返回 newTab 即可



if (oldTab != null) {

// 开始遍历原数组,进行数据迁移。
for (int j = 0; j < oldCap; ++j) {

Node<K,V> e;

if ((e = oldTab[j]) != null) {

oldTab[j] = null;

// 如果该数组位置上只有单个元素,那就简单了,简单迁移这个元素就可以了
if (e.next == null)

// 根据 e.hash & (newCap - 1) 计算 oldTab 中的元素在 newTab 中的 index
newTab[e.hash & (newCap - 1)] = e;

// 如果是红黑树,具体就不展开了
else if (e instanceof TreeNode)

((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

else {

// 这块是处理链表的情况,
// 需要将此链表拆成两个链表,放到新的数组中,并且保留原来的先后顺序
// loHead、loTail 对应一条链表,hiHead、hiTail 对应另一条链表
Node<K,V> loHead = null, loTail = null;

Node<K,V> hiHead = null, hiTail = null;

Node<K,V> next;

do {

next = e.next;

// oldTab 中计算 index 时使用的是 e.hash & (oldCap -1),注意这里的区别
// oldTab[i] 的元素只会被分配到 newTab[i] 或 newTab[i + oldCap] 上
if ((e.hash & oldCap) == 0) {

if (loTail == null)

loHead = e;

else

loTail.next = e;

loTail = e;

}

else {

if (hiTail == null)

hiHead = e;

else

hiTail.next = e;

hiTail = e;

}

} while ((e = next) != null);

if (loTail != null) {

loTail.next = null;

// 第一条链表
newTab[j] = loHead;

}

if (hiTail != null) {

hiTail.next = null;

// 第二条链表的新的位置是 j + oldCap,这个很好理解
newTab[j + oldCap] = hiHead;

}

}

}

}

}

return newTab;

}

get 过程分析

  1. 计算 key 的 hash 值,根据 hash 值找到对应数组下标: hash & (length-1)

  2. 判断数组该位置处的元素是否刚好就是我们要找的,如果不是,走第三步

  3. 判断该元素类型是否是 TreeNode,如果是,用红黑树的方法取数据,如果不是,走第四步

  4. 遍历链表,直到找到相等(==或equals)的 key

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    public V get(Object key) {

    Node<K,V> e;

    return (e = getNode(hash(key), key)) == null ? null : e.value;

    }


    final Node<K,V> getNode(int hash, Object key) {

    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

    if ((tab = table) != null && (n = tab.length) > 0 &&
    (first = tab[(n - 1) & hash]) != null) {

    // 判断第一个节点是否为所需
    if (first.hash == hash && // always check first node
    ((k = first.key) == key || (key != null && key.equals(k))))
    return first;

    if ((e = first.next) != null) {

    // 判断是否是红黑树
    if (first instanceof TreeNode)
    return ((TreeNode<K,V>)first).getTreeNode(hash, key);

    // 链表遍历
    do {
    if (e.hash == hash &&
    ((k = e.key) == key || (key != null && key.equals(k))))
    return e;
    } while ((e = e.next) != null);

    }

    }
    return null;

    }

remove

remove 方法有两个重载版本,本人将其定义为“强匹配”(key 和 value 都匹配)和“弱匹配”(key 匹配即可),接下来只针对“弱匹配”分析,“强匹配”大同小异

remove 的基本思路是通过 key 的 hash 值得到其在 table 中的 index 值,然后对该 index 位置上的链表或树进行查找,若找到则删除且返回旧值(旧值可以为 null),找不到则返回 null

源码如下

1
2
3
4
5
6
7
8
public V remove(Object key) {

Node<K,V> e;

return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// matchValue 为 false 则代表“弱匹配”
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {

Node<K,V>[] tab; Node<K,V> p; int n, index;

if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {

// node 代表与 key 相匹配的节点
Node<K,V> node = null, e; K k; V v;

// 若头节点即为要找的节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))

node = p;


else if ((e = p.next) != null) {

// 在红黑树中查找节点,暂不分析
if (p instanceof TreeNode)

node = ((TreeNode<K,V>)p).getTreeNode(hash, key);

// 在链表中查找节点
else {

// 此循环最终的结果是
// 若找到匹配的几点,则 node 指向该节点,p 指向该节点的上一个节点
// 若找不到,则 node 为 null,p 指向链表的尾节点
do {

if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {

node = e;

break;

}

p = e;

} while ((e = e.next) != null);

}

}

// node == null 代表找不到匹配的节点,直接返回 null
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {

// 红黑树删除节点,暂不分析
if (node instanceof TreeNode)

((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);

// 若 node == p,则代表 table[index] 的第一个节点即为要找的节点
else if (node == p)

tab[index] = node.next;

else

p.next = node.next;

++modCount;

--size;

afterNodeRemoval(node);

return node;

}

}

return null;

}

HashMap 中的红黑树

参阅HashMap 在 JDK 1.8 后新增的红黑树结构


HashMap 线程不安全的原因

主要体现在 resize 时的死循环及使用迭代器时的 fast-fail 上

单线程 resize

多线程并发下的 resize





至此出现循环链表

Fast-fail

产生原因

在使用迭代器的过程中如果 HashMap 被修改,那么 ConcurrentModificationException 将被抛出,也即 Fast-fail 策略

通过判断迭代器自己的 modCount 和 HashMap 的 modCount 是否相等来判断是否在迭代的过程中 HashMap 经过修改

线程安全解决方案

需要保证只通过 HashMap 本身或者只通过 Iterator 去修改数据,不能在 Iterator 使用结束之前使用 HashMap 本身的方法修改数据

多线程条件下,可使用 Collections.synchronizedMap 方法构造出一个同步 Map,或者直接使用线程安全的 ConcurrentHashMap


Java8 ConcurrentHashMap

Java7 ConcurrentHashMap

采用锁分段机制

简单地说,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全

ConcurrentHashMap 中重要的属性

  • concurrencyLevel :默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的

  • initialCapacity :初始容量,这个值指的是整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment

  • loadFactor :负载因子,之前我们说了,Segment 数组不可以扩容,所以这个负载因子是给每个 Segment 内部使用的

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 这构造函数里,什么都不干

public ConcurrentHashMap() {

}

public ConcurrentHashMap(int initialCapacity) {

if (initialCapacity < 0)

throw new IllegalArgumentException();

int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?

MAXIMUM_CAPACITY :

tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));

this.sizeCtl = cap;

}

put 过程分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
public V put(K key, V value) {

return putVal(key, value, false);

}

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

if (key == null || value == null) throw new NullPointerException();

// 得到 hash 值

int hash = spread(key.hashCode());

// 用于记录相应链表的长度

int binCount = 0;

for (Node<K,V>[] tab = table;;) {

Node<K,V> f; int n, i, fh;

// 如果数组"空",进行数组初始化

if (tab == null || (n = tab.length) == 0)

// 初始化数组,后面会详细介绍

tab = initTable();



// 找该 hash 值对应的数组下标,得到第一个节点 f

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {

// 如果数组该位置为空,

// 用一次 CAS 操作将这个新值放入其中即可,这个 put 操作差不多就结束了,可以拉到最后面了

// 如果 CAS 失败,那就是有并发操作,进到下一个循环就好了

if (casTabAt(tab, i, null,

new Node<K,V>(hash, key, value, null)))

break; // no lock when adding to empty bin

}

// hash 居然可以等于 MOVED,这个需要到后面才能看明白,不过从名字上也能猜到,肯定是因为在扩容

else if ((fh = f.hash) == MOVED)

// 帮助数据迁移,这个等到看完数据迁移部分的介绍后,再理解这个就很简单了

tab = helpTransfer(tab, f);



else { // 到这里就是说,f 是该位置的头结点,而且不为空



V oldVal = null;

// 获取数组该位置的头结点的监视器锁

synchronized (f) {

if (tabAt(tab, i) == f) {

if (fh >= 0) { // 头结点的 hash 值大于 0,说明是链表

// 用于累加,记录链表的长度

binCount = 1;

// 遍历链表

for (Node<K,V> e = f;; ++binCount) {

K ek;

// 如果发现了"相等"的 key,判断是否要进行值覆盖,然后也就可以 break 了

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;

if ((e = e.next) == null) {

pred.next = new Node<K,V>(hash, key,

value, null);

break;

}

}

}

else if (f instanceof TreeBin) { // 红黑树

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;

}

}

}

}

// binCount != 0 说明上面在做链表操作

if (binCount != 0) {

// 判断是否要将链表转换为红黑树,临界值和 HashMap 一样,也是 8

if (binCount >= TREEIFY_THRESHOLD)

// 这个方法和 HashMap 中稍微有一点点不同,那就是它不是一定会进行红黑树转换,

// 如果当前数组的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树

// 具体源码我们就不看了,扩容部分后面说

treeifyBin(tab, i);

if (oldVal != null)

return oldVal;

break;

}

}

}

//

addCount(1L, binCount);

return null;

}

初始化数组 :initTable

初始化方法中的并发问题是通过对 sizeCtl 进行一个 CAS 操作来控制的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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(); // lost initialization race; just spin

// CAS 一下,将 sizeCtl 设置为 -1,代表抢到了锁

else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {

try {

if ((tab = table) == null || tab.length == 0) {

// DEFAULT_CAPACITY 默认初始容量是 16

int n = (sc > 0) ? sc : DEFAULT_CAPACITY;

// 初始化数组,长度为 16 或初始化时提供的长度

Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];

// 将这个数组赋值给 table,table 是 volatile 的

table = tab = nt;

// 如果 n 为 16 的话,那么这里 sc = 12

// 其实就是 0.75 * n

sc = n - (n >>> 2);

}

} finally {

// 设置 sizeCtl 为 sc,我们就当是 12 吧

sizeCtl = sc;

}

break;

}

}

return tab;

}

链表转红黑树 :treeifyBin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

private final void treeifyBin(Node<K,V>[] tab, int index) {

Node<K,V> b; int n, sc;

if (tab != null) {

// MIN_TREEIFY_CAPACITY 为 64

// 所以,如果数组长度小于 64 的时候,其实也就是 32 或者 16 或者更小的时候,会进行数组扩容

if ((n = tab.length) < MIN_TREEIFY_CAPACITY)

// 后面我们再详细分析这个方法

tryPresize(n << 1);

// b 是头结点

else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {

// 加锁

synchronized (b) {



if (tabAt(tab, index) == b) {

// 下面就是遍历链表,建立一颗红黑树

TreeNode<K,V> hd = null, tl = null;

for (Node<K,V> e = b; e != null; e = e.next) {

TreeNode<K,V> p =

new TreeNode<K,V>(e.hash, e.key, e.val,

null, null);

if ((p.prev = tl) == null)

hd = p;

else

tl.next = p;

tl = p;

}

// 将红黑树设置到数组相应位置中

setTabAt(tab, index, new TreeBin<K,V>(hd));

}

}

}

}

}

扩容:tryPresize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// 首先要说明的是,方法参数 size 传进来的时候就已经翻了倍了

private final void tryPresize(int size) {

// c:size 的 1.5 倍,再加 1,再往上取最近的 2 的 n 次方。

int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :

tableSizeFor(size + (size >>> 1) + 1);

int sc;

while ((sc = sizeCtl) >= 0) {

Node<K,V>[] tab = table; int n;



// 这个 if 分支和之前说的初始化数组的代码基本上是一样的,在这里,我们可以不用管这块代码

if (tab == null || (n = tab.length) == 0) {

n = (sc > c) ? sc : c;

if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {

try {

if (table == tab) {

@SuppressWarnings("unchecked")

Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];

table = nt;

sc = n - (n >>> 2); // 0.75 * n

}

} finally {

sizeCtl = sc;

}

}

}

else if (c <= sc || n >= MAXIMUM_CAPACITY)

break;

else if (tab == table) {

// 我没看懂 rs 的真正含义是什么,不过也关系不大

int rs = resizeStamp(n);



if (sc < 0) {

Node<K,V>[] nt;

if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||

sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||

transferIndex <= 0)

break;

// 2. 用 CAS 将 sizeCtl 加 1,然后执行 transfer 方法

// 此时 nextTab 不为 null

if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))

transfer(tab, nt);

}

// 1. 将 sizeCtl 设置为 (rs << RESIZE_STAMP_SHIFT) + 2)

// 我是没看懂这个值真正的意义是什么?不过可以计算出来的是,结果是一个比较大的负数

// 调用 transfer 方法,此时 nextTab 参数为 null

else if (U.compareAndSwapInt(this, SIZECTL, sc,

(rs << RESIZE_STAMP_SHIFT) + 2))

transfer(tab, null);

}

}

}

数据迁移:transfer

To do

get 过程分析

  1. 计算 hash 值

  2. 根据 hash 值找到数组对应位置: (n – 1) & h

  3. 根据该位置处结点性质进行相应查找

  • 如果该位置为 null,那么直接返回 null 就可以了

  • 如果该位置处的节点刚好就是我们需要的,返回该节点的值即可

  • 如果该位置节点的 hash 值小于 0,说明正在扩容,或者是红黑树,后面我们再介绍 find 方法

  • 如果以上 3 条都不满足,那就是链表,进行遍历比对即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    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;

    }

    // 如果头结点的 hash 小于 0,说明 正在扩容,或者该位置是红黑树

    else if (eh < 0)

    // 参考 ForwardingNode.find(int h, Object k) 和 TreeBin.find(int h, Object k)

    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;

    }

参考链接