一、概述
ConcurrentHashMap是(java.util.concurrent包)的重要成员,它是HashMap的一个线程安全的、支持高效并发的版本,ConcurrentHashMap和HashMap一样,是一个存放键值对的容器。使用hash算法来获取值的地址,因此时间复杂度是O(1),查询非常快。
二、为什么要使用 ConcurrentHashMap?
1.在并发编程中,jdk1.7的情况下使用 HashMap 可能造成死循环,而jdk1.8 中有可能会造成数据丢失。
2.HashTable 虽然也是线程安全的,但是效率非常低下。
三、ConcurrentHashMap 结构
1.在jdk1.7版本
(1)ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成。
(2)主要实现原理是实现了锁分离的思路,采用分段锁的机制,实现并发的更新操作。
(3)底层采用数组+链表的存储结构,包括两个核心静态内部类Segment和HashEntry。
(4)Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到 的锁分离技术。每一个Segment元素存储的是HashEntry数组+链表(若干个桶),这个和HashMap的数据存储结构一样。
(5)HashEntry用来封装映射表的键值对,每个桶是由若干个HashEntry对象链接起来的链表。
2.在jdk1.8后
(1)取消了Segment类,直接用table数组存储键值对。采用Node + CAS + Synchronized来保证并发安全。
(2)Node数据结构比较简单,就是一个链表,但是只允许对数据进行查找,不允许进行修改。
(3)当HashEntry对象组成的链表长度超过8时,或数组长度小于64 就会扩容,则链表转换为红黑树,提升性能。底层变更为数组+链表+红黑树。
四、ConcurrentHashMap底层原理
以jdk1.8为例:
1.Node节点数字用的是volatile修饰。
//ConcurrentHashMap使用volatile修饰节点数组,保证其可见性,禁止指令重排。
transient volatile Node<K,V>[] table;
3.ConcurrentHashMap的put()方法。
//put()方法直接调用putVal()方法
public V put(K key, V value) {
return putVal(key, value, false);
}
//所以直接看putVal()方法。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
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();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
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;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
从上面看put方法并未用synchronized修饰。
其put大致过程如下:
(1)根据 key 计算出 hashcode,然后开始遍历 table;
(2)判断是否需要初始化;
(3)f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
(4)如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
(5)如果都不满足,则利用 synchronized 锁写入数据。
(6)如果数量大于 TREEIFY_THRESHOLD ,则要转换为红黑树。
4.ConcurrentHashMap的get()方法
//ConcurrentHashMap的get()方法是不加锁的,方法内部也没加锁。
public V get(Object key)
那么为什么ConcurrentHashMap的get()方法是不加锁的,因为table有volatile关键字修饰,保证每次获取值都是最新的。
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;
}
// 如果头节点的 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;
}
get过程:
(1)首先根据key计算出来的 hashcode 寻址,如果就在桶上那么直接返回值,
(2)如果是红黑树那就按照树的方式获取值,
(3)都不满足那就按照链表的方式遍历获取值。
总结:
1.高并发性能:ConcurrentHashMap JDK1.8 在 1.7 的数据结构上做了大的改动,采用了CAS+synchronized+Node+红黑树实现,结合了原子操作的优势,同时也保持了锁的简洁性。
2.扩容机制:ConcurrentHashMap 在扩容时采用了更为高效的复制方式,通过创建新的数组并逐步替换旧的数组来完成,这样可以减少对于锁的竞争。
本文详细介绍了ConcurrentHashMap在Java中的重要性,尤其是在并发编程中的优势,以及其在不同JDK版本下的数据结构和底层原理,重点讲解了put()和get()方法的工作机制,以及1.8版本中采用的CAS、synchronized和红黑树等技术以提高并发性能和扩容效率。
4300

被折叠的 条评论
为什么被折叠?



