面试官直接灵魂拷问谈谈ConcurrentHashMap的深度间接,我直接...🙊🙊
咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE相关知识点了,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~
🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8
前言
在并发编程中,线程安全是一个非常重要的概念。而在Java中,ConcurrentHashMap是一个经典的线程安全的容器,其在多线程并发访问时性能表现优异。所以此文,我就给大家来聊聊它,其中它也是热门面试题八股文之一呢。
摘要
本文将从源代码解析、应用场景案例、优缺点分析等多方面,对Java中的ConcurrentHashMap进行全面深入地探讨和分析,就差手把手带着同学们捋一遍了。
ConcurrentHashMap解读
概述
首先,ConcurrentHashMap是Java中的一个线程安全的哈希表实现。它 在 JDK 1.7 和 JDK 1.8 中的实现有显著的不同,这些差异主要体现在它们的内部结构和并发控制机制上。
结构
对于JDK1.7而言:
在 JDK 1.7 中,ConcurrentHashMap 使用了分段锁(Segment)的机制。这种设计允许多个线程同时对不同的段(Segment)进行操作,从而提高了并发性能。每个段实际上是一个独立的 HashEntry 数组,它有自己的锁。当一个线程需要修改一个段时,它将获得该段的锁,而其他线程仍然可以对其他段进行读取操作。
特点:
- 分段锁提供了一定程度的并发性,但锁的粒度相对较大。
- 每个段是一个独立的小型哈希表,有自己的扩容机制。
- 性能受限于段的数量和大小,以及锁的竞争。
对于JDK1.8而言:
JDK 1.8 完全重写了 ConcurrentHashMap
的实现,放弃了分段锁机制,转而使用一种基于 CAS(Compare-And-Swap)和原子变量的锁机制。在 JDK 1.8 中,ConcurrentHashMap
使用了一个数组加上链表(在链表过长时转换为红黑树)的数据结构。这种设计使得 ConcurrentHashMap
能够在高并发场景下提供更好的性能。
特点:
- 使用了更细粒度的锁,每个链表节点(或红黑树节点)都有自己的锁,这被称为锁分离(lock striping)。
- 通过使用红黑树来优化长链表的查找性能。
- 支持更高的并发级别,因为锁的竞争减少了。
- 扩容操作更加高效,因为它可以在不影响其他元素的情况下,逐个处理每个桶(bucket)。
本质和区别
本质:
- 两者都是为了提供线程安全的高效哈希表操作。
- 都使用了哈希表数据结构,并在必要时进行扩容。
区别:
- 锁机制:JDK 1.7 使用分段锁,而 JDK 1.8 使用锁分离和原子操作。
- 并发性能:JDK 1.8 的实现提供了更高的并发性能,因为它减少了锁的竞争。
- 数据结构:JDK 1.8 引入了红黑树来优化长链表的性能。
- 内存占用:JDK 1.8 的实现可能会有更高的内存占用,因为它使用了更多的锁和更复杂的数据结构。
- 性能调优:JDK 1.8 提供了更多的构造函数参数来调优性能,例如初始容量和负载因子。
总的来说,JDK 1.8 中的 ConcurrentHashMap
在设计上更加现代化,提供了更好的性能和更高的并发性。这使得它成为处理高并发场景的首选数据结构之一。
源代码解读
ConcurrentHashMap
继承自AbstractMap类,实现了ConcurrentMap
接口。它主要由Segment
和HashEntry
两个类组成。
如下是部分源码截图:
Segment
类
Segment
是ConcurrentHashMap
的核心。Segment
是一个锁,能保证多线程环境下的安全。每个Segment
默认关联着一个数组,也就是说,ConcurrentHashMap
的数据结构是由多个Segment及其数组组成的。
Segment
通过继承ReentrantLock
来实现锁的机制。在读取操作的时候,可以允许多个线程同时访问同一个Segment
,提高并发性能。而在写入操作时,只能保证同一个Segment
中的操作是线程安全的,并不保证多个Segment
之间的并发性。
Segment
内部还定义了一个内部类HashEntry
。因为ConcurrentHashMap
是基于哈希表实现的,因此需要一个哈希表的数据结构来存储键值对。HashEntry
就是HashMap的一个节点,用于存储键值对。
HashEntry类
HashEntry
是ConcurrentHashMap
中的键值对节点。它的定义如下:
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
final boolean casValue(V cmp, V val) {
return UNSAFE.compareAndSwapObject(this, valueOffset, cmp, val);
}
final boolean casNext(HashEntry<K,V> cmp, HashEntry<K,V> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
// 其他方法...
}
从上面的源码可以看出,HashEntry包含一个哈希值、键、值、下一个节点等信息。其中,volatile
关键字修饰的value和next可以保证多线程环境下的可见性和有序性,从而保证线程安全。另外,HashEntry中还定义了两个方法casValue和casNext,用于实现无锁操作,提高并发性能。
拓展:
这是一个 HashEntry
类,作为 ConcurrentHashMap
中的元素存储。
它包含的属性有:
- hash:键的哈希值,计算方式是通过 key 的 hashCode() 方法得到。
- key:键,可以为 null。
- value:值,可以为 null。
- next:下一个元素的引用,用于解决哈希冲突时使用。
其中,value 和 next 都是 volatile 的,保证多线程环境下的可见性。
还有两个 cas 方法:
- casValue:尝试原子地将当前 entry 的 value 字段从 cmp 替换为 val,如果替换成功则返回 true,否则返回 false。
- casNext:尝试原子地将当前 entry 的 next 字段从 cmp 替换为 val,如果替换成功则返回 true,否则返回 false。
这两个 cas 方法都是使用了 java.util.concurrent.atomic 包下的 Unsafe 类,即使用了底层的机器指令实现的原子操作,保证了在多线程环境下的线程安全性。
put方法
ConcurrentHashMap
中插入元素的方法是put方法。其实现过程大致如下:
- 根据key的哈希值计算出Segment的下标。
- 如果Segment为空,使用CAS操作进行创建。
- 如果Segment已经存在,则使用锁进行保证多线程环境下的安全。
- 在Segment中查找是否存在相同的key,如果存在,则替换掉旧值;如果不存在,则在Segment的链表中添加一个新节点。
代码实现如下所示:
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
拓展:
在put方法中,先根据key的哈希值计算出Segment的下标,然后查找是否已经存在相同key的节点。如果存在,则将该节点的值替换成新值;如果不存在,则创建一个新的节点,并添加到Segment的链表中。
在创建新节点时,要使用CAS操作进行无锁操作,保证多线程环境下的安全。
该代码片段是Java并发哈希表ConcurrentHashMap中的put方法。它接收一个键值对,尝试将其加入哈希表中。首先,它使用哈希函数计算键的哈希值并确定要使用哪个哈希段。然后,它调用相应的哈希段的put方法。
在哈希段的put方法中,它首先尝试获取锁,如果锁不可用,则调用scanAndLockForPut方法。该方法扫描哈希表中的槽位,查找与键匹配的节点。如果找到,则返回该节点的值;否则,创建一个新的节点并将其放入哈希表中。如果哈希表已经过度填充,则重新调整大小并重新散列所有节点。
最后,put方法返回原来键的值,如果该键不存在,则返回null。
get方法
ConcurrentHashMap中获取元素的方法是get。其实现过程大致如下:
- 根据key的哈希值计算出Segment的下标。
- 在Segment中查询是否存在相同key的节点。
- 如果存在,则返回节点的值;如果不存在,则返回null。
代码实现如下所示:
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE));
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
在get方法中,也是根据key的哈希值计算出Segment的下标,然后在该Segment的哈希链表中查询是否存在相同key的节点,如果存在,则返回该节点的值;否则返回null。
拓展:
这段代码实现了ConcurrentHashMap中的get()方法,用于获取指定key对应的value。
具体实现是先根据key计算出对应的hash值,并根据hash值计算出对应的Segment(分区)对象。如果该Segment存在并且对应的table(哈希桶数组)也存在,就从table中查找对应的entry(键值对)。如果找到了,则返回其value值,否则返回null。
值得注意的是,该方法在获取Segment和table对象时使用了volatile修饰符,以确保多线程环境下的可见性。同时,也利用了UNSAFE类来优化访问过程,提高效率。
size方法
ConcurrentHashMap中获取元素个数的方法是size。其实现过程大致如下:
- 遍历所有的Segment,获取每个Segment中的元素个数。
- 对所有Segment中的元素个数求和,得到ConcurrentHashMap中元素的总个数。
代码实现如下所示:
public int size() {
long count = 0L;
final Segment<K,V>[] segments = this.segments;
for (int i = 0; i < segments.length; ++i) {
Segment<K,V> segment = segments[i];
if (segment != null)
count += segment.getCount();
}
return count >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int)count;
}
在计算元素个数时,使用了volatile修饰的count成员变量,保证了线程安全。
拓展:
这是一个用于计算ConcurrentHashMap中元素数量的方法。该方法首先将所有Segment的元素数量累加起来,最终返回总数量。
具体实现方式为:遍历所有Segment,若Segment不为null,则获取该Segment中元素的数量并累加到count中。最终返回的是int类型,因此当count的值大于或等于Integer.MAX_VALUE时,返回Integer.MAX_VALUE,否则返回count的值。
需要注意的是,由于ConcurrentHashMap采用分段锁策略来保证并发访问的效率,因此在计算元素数量时需要遍历所有的Segment,这可能会影响计算速度。
应用场景案例
ConcurrentHashMap是一个线程安全的哈希表实现,在多线程并发访问时性能表现优异。因此,它适用于以下场景:
-
在高并发的web应用中,可以使用ConcurrentHashMap存储session信息,以确保多个请求同时访问session时的线程安全性。
-
在多线程的数据处理场景中,可以使用ConcurrentHashMap存储操作的中间结果,以避免使用锁机制造成的性能瓶颈。
-
在多线程的缓存场景中,可以使用ConcurrentHashMap作为缓存容器,以支持高并发的读写操作。
-
在分布式系统中,可以使用ConcurrentHashMap作为本地缓存,以减少远程调用的次数,提高系统性能。
优缺点分析
优点
-
线程安全:ConcurrentHashMap是一个线程安全的哈希表实现,在多线程并发访问时性能表现优异。
-
高并发性:ConcurrentHashMap的内部实现是基于Segment的,每个Segment内部都有一个锁来控制并发访问,从而使得多个线程能够同时读取数据,提高了并发性能。
-
高效性:ConcurrentHashMap的性能表现很优秀。在高并发读写场景中,性能比Hashtable和SynchronizedMap要好得多。
-
可扩展性:ConcurrentHashMap的内部结构是基于哈希表的,因此,在元素数量增加时,可以通过调整Segment的数量来扩展ConcurrentHashMap的容量,从而保证元素的均衡分布。
缺点
-
内存占用:由于ConcurrentHashMap的内部实现是基于Segment的,因此它会占用更多的内存空间。
-
不支持空值:ConcurrentHashMap的值不能为空。
-
不保证元素的排列顺序:ConcurrentHashMap不保证元素的排列顺序,因此不能用于需要保证元素顺序的场景。
测试用例
测试代码演示
下面是一个简单的 ConcurrentHashMap
测试用例:
/**
* ConcurrentHashMap示例演示
*
* @Author bug菌
* @Source 公众号:猿圈奇妙屋
* @Date 2023-11-06 16:53
*/
public class ConcurrentHashMapTest {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 添加元素
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
// 打印元素
System.out.println(map);
// 获取元素
Integer one = map.get("one");
System.out.println("one = " + one);
// 移除元素
Integer removed = map.remove("two");
System.out.println("removed = " + removed);
System.out.println(map);
// 替换元素
Integer replaced = map.replace("one", 100);
System.out.println("replaced = " + replaced);
System.out.println(map);
// 遍历元素
map.forEach((key, value) -> {
System.out.println(key + " = " + value);
});
}
}
测试结果
根据如上测试用例,本地测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加更多的测试数据或测试方法,进行熟练学习以此加深理解。
输出结果:
{two=2, one=1, three=3}
one = 1
removed = 2
{one=1, three=3}
replaced = 1
{one=100, three=3}
one = 100
three = 3
实际执行结果如下:
测试代码分析
根据如上测试用例,在此我给大家进行深入详细的解读一下测试代码,以便于更多的同学能够理解并加深印象。 该示例演示了ConcurrentHashMap的基本操作,包括添加元素、获取元素、移除元素、替换元素和遍历元素等。其中,ConcurrentHashMap是线程安全的哈希表实现,相较于HashTable和HashMap,其性能更优。在多线程环境下,ConcurrentHashMap能够提供更好的并发性能和可伸缩性。
小结
ConcurrentHashMap是Java中的一个线程安全的哈希表实现,在高并发读写场景中性能表现优异。它的内部实现是基于Segment的,通过锁机制来保证线程安全性,并且可以通过增加Segment的数量来扩展容量,具有很好的可扩展性。但是,由于它的内部结构是基于哈希表的,因此会占用更多的内存空间。同时,ConcurrentHashMap不支持空值,并且不保证元素的排列顺序,需要根据具体的场景选择使用。
总结
本文详细介绍了Java中的线程安全容器ConcurrentHashMap,包括其源码解析、应用场景案例和优缺点分析。ConcurrentHashMap采用了分段锁和哈希表的结构实现,能够保证线程安全和高并发性能。同时,它也具有可扩展性和高效性。但是,ConcurrentHashMap也存在一些缺点,如占用更多的内存空间,不支持空值等。总之,在适当的场景中,使用ConcurrentHashMap能够提高程序的性能和可靠性。
... ...
好啦,这期的内容就基本接近尾声啦,若你想学习更多,你可以看看专栏的导读篇《「滚雪球学Java」教程导航帖》,本专栏致力打造最硬核 Java 零基础系列学习内容,🚀打造全网精品硬核专栏,带你直线超车;欢迎大家订阅持续学习。功不唐捐,久久为功!
「赠人玫瑰,手留余香」,咱们下期拜拜~~
附录源码
如上涉及所有源码均已上传同步在「Gitee」,提供给同学们一对一参考学习,辅助你更迅速的掌握。
☀️建议/推荐你
无论你是计算机专业的学生,还是对编程感兴趣的跨专业小白,都建议直接入手「滚雪球学Java」专栏;该专栏不仅免费,bug菌还郑重承诺,只要你学习此专栏,均能入门并理解Java SE,以全网最快速掌握Java语言,每章节源码均同步「Gitee」,你真值得拥有;学习就像滚雪球一样,越滚越大,带你指数级提升。
码字不易,如果这篇文章对你有所帮助,帮忙给bugj菌来个一键三连(关注、点赞、收藏) ,您的支持就是我坚持写作分享知识点传播技术的最大动力。
📣关于我
转载自:https://juejin.cn/post/7367631026250743847