在这个金九银十的时期,应该有不少小伙伴都在为面试准备着
所以我也整了一些面试文档
短期面试攻略 这样背面试题事半功倍简直赢麻了
需要的小伙伴三联哦!
我们现在继续讲讲ConcurrentHashMap 实现原理是什么?
JDK1.7
在JDK1.7中,首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。在 JDK1.7 中,ConcurrentHashMap 采用 Segment + HashEntry 的方式进行实现,
结构如下:一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁
-
该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;
-
Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。
JDK1.8
在 JDK1.8 中,放弃了 Segment 臃肿的设计,取而代之的是采用 Node + CAS +Synchronized 来保证并发安全进行实现,synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。
结构如下:
附加源码,有需要的可以看看
插入元素过程源码解读:
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 检查参数,如果key或value为null则抛出NullPointerException
if (key == null || value == null)
throw new NullPointerException();
// 对key的hashCode进行扩展处理,以增加散列性能
int hash = spread(key.hashCode());
int binCount = 0; // 记录链表长度
for (Node<K,V>[] tab = table;;) { // 无限循环,直到成功插入或者扩容
Node<K,V> f; int n, i, fh;
// 如果table为空,进行初始化
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关键字对桶上的链表进行同步操作
synchronized (f) {
if (tabAt(tab, i) == f) { // 再次检查桶中的第一个节点是否是f
if (fh >= 0) {
// 遍历链表,查找是否有相同的key存在
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; // 找到相同的key,记录旧值
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; // 返回null表示没有旧值
}
putVal方法用于将键值对插入ConcurrentHashMap中。在并发环境下,通过对不同的桶进行加锁操作,保证了线程安全性。具体的插入过程如下:
- 检查参数,如果key或value为null则抛出NullPointerException。
- 对key的hashCode进行扩展处理,以增加散列性能。
- 进入无限循环,直到成功插入或者扩容。
- 如果table为空,进行初始化。
- 如果桶位置为空,直接将新节点插入到桶中。
- 如果桶处于扩容状态,则帮助进行扩容。
- 如果桶中是链表节点(不是树形节点),遍历链表查找是否有相同的key存在。
- 如果桶中是树形节点,插入或更新树形节点。
- 插入或更新成功后,根据链表长度判断是否需要将链表转换为树形节点。
- 更新计数器。
- 返回旧值。
在高并发场景下,ConcurrentHashMap通过细粒度的锁和分段的结构,提供了更好的并发性能和扩容能力,同时保持线程安全。