老生常谈java常面 HashMap:
首先都知道HashMap是线程不安全的那么聊一聊HashMap:
- 默认初始化大小 16
- 负载因子 0.75 即当存放数据数量达到当前预计数量的 0.75时 会发生扩容
- 扩容倍数 2 即发生扩容会是之前预计数量的两倍
ok 基础说完了那就聊聊为什么会不安全
首先说jdk1.7的不安全
重点代码如下
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
-
死循环问题
明确的是 1.7采用链表加数组的形式存储 而且如果存在hash碰撞它采用头插法存放数据
上述 来重点看最后三行 赋值操作
在对table进行扩容到newTable后,需要将原来数据转移到newTable中, 这里可以看出在转移元素的过程中,使用的是头插法,也就是链表的顺序会翻转,这里也是形成死循环的关键点。下面进行详细分析。
假如线程A刚刚执行完 newTable[i] = e; 挂起
而线程B执行完成了该操作那么就会出现 冲突的数据即 死循环 -
扩容丢数据的问题
同样线程A执行到 newTable[i] = e; 挂起
此时线程B完成了 resize()操作
此时切回线程A 执行 **e = next;**此时next会被B修改从而出现数据丢失并且有可能出现空值导致死循环出现
那么继续 jdk1.8:
首先还是重要代码
1final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
2 boolean evict) {
3 Node<K,V>[] tab; Node<K,V> p; int n, i;
4if ((tab = table) == null || (n = tab.length) == 0)
5 n = (tab = resize()).length;
6if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素
7 tab[i] = newNode(hash, key, value, null);
8else {
9 Node<K,V> e; K k;
10if (p.hash == hash &&
11 ((k = p.key) == key || (key != null && key.equals(k))))
12 e = p;
13elseif (p instanceof TreeNode)
14 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
15else {
16for (int binCount = 0; ; ++binCount) {
17if ((e = p.next) == null) {
18 p.next = newNode(hash, key, value, null);
19if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
20 treeifyBin(tab, hash);
21break;
22 }
23if (e.hash == hash &&
24 ((k = e.key) == key || (key != null && key.equals(k))))
25break;
26 p = e;
27 }
28 }
29if (e != null) { // existing mapping for key
30 V oldValue = e.value;
31if (!onlyIfAbsent || oldValue == null)
32 e.value = value;
33 afterNodeAccess(e);
34return oldValue;
35 }
36 }
37 ++modCount;
38if (++size > threshold)
39 resize();
40 afterNodeInsertion(evict);
41returnnull;
42 }
- 数据丢失问题
jdk1.8中HashMap中put操作的主函数, 注意第6行代码,如果没有hash碰撞则会直接插入元素。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入第6行代码中。假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。