咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE相关知识点了,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~
🏆本文收录于 **[「滚雪球学Java」 ](https://blog.csdn.net/weixin_43970743/category_9600553.html)专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅**!持续更新中,up!up!up!!
环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8
在并发编程中,线程安全是一个非常重要的概念。而在Java中,ConcurrentHashMap是一个经典的线程安全的容器,其在多线程并发访问时性能表现优异。所以此文,我就给大家来聊聊它,其中它也是热门面试题八股文之一呢。
本文将从源代码解析、应用场景案例、优缺点分析等多方面,对Java中的ConcurrentHashMap进行全面深入地探讨和分析,就差手把手带着同学们捋一遍了。
首先,ConcurrentHashMap是Java中的一个线程安全的哈希表实现。它 在 JDK 1.7 和 JDK 1.8 中的实现有显著的不同,这些差异主要体现在它们的内部结构和并发控制机制上。
在 JDK 1.7 中,ConcurrentHashMap 使用了分段锁(Segment)的机制。这种设计允许多个线程同时对不同的段(Segment)进行操作,从而提高了并发性能。每个段实际上是一个独立的 HashEntry 数组,它有自己的锁。当一个线程需要修改一个段时,它将获得该段的锁,而其他线程仍然可以对其他段进行读取操作。
特点:
JDK 1.8 完全重写了 ConcurrentHashMap
的实现,放弃了分段锁机制,转而使用一种基于 CAS(Compare-And-Swap)和原子变量的锁机制。在 JDK 1.8 中,ConcurrentHashMap
使用了一个数组加上链表(在链表过长时转换为红黑树)的数据结构。这种设计使得 ConcurrentHashMap
能够在高并发场景下提供更好的性能。
特点:
本质:
区别:
总的来说,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
是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
中的元素存储。
它包含的属性有:
其中,value 和 next 都是 volatile 的,保证多线程环境下的可见性。
还有两个 cas 方法:
这两个 cas 方法都是使用了 java.util.concurrent.atomic 包下的 Unsafe 类,即使用了底层的机器指令实现的原子操作,保证了在多线程环境下的线程安全性。
ConcurrentHashMap
中插入元素的方法是put方法。其实现过程大致如下:
代码实现如下所示:
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。
ConcurrentHashMap中获取元素的方法是get。其实现过程大致如下:
代码实现如下所示:
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类来优化访问过程,提高效率。
ConcurrentHashMap中获取元素个数的方法是size。其实现过程大致如下:
代码实现如下所示:
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是一个线程安全的哈希表实现,在多线程并发访问时性能表现优异。因此,它适用于以下场景:
下面是一个简单的 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」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门Java编程,就像滚雪球一样,越滚越大,指数级提升。
码字不易,如果这篇文章对你有所帮助,帮忙给bug菌来个一键三连(关注、点赞、收藏) ,您的支持就是我坚持写作分享知识点传播技术的最大动力。 同时也推荐大家关注我的硬核公众号:「猿圈奇妙屋」 ;以第一手学习bug菌的首发干货,不仅能学习更多技术硬货,还可白嫖最新BAT大厂面试真题、4000G Pdf技术书籍、万份简历/PPT模板、技术文章Markdown文档等海量资料,你想要的我都有!
我是bug菌,CSDN | 掘金 | infoQ | 51CTO 等社区博客专家,历届博客之星Top30,掘金年度人气作者Top40,51CTO年度博主Top12,掘金等平台签约作者,华为云 | 阿里云| 腾讯云等社区优质创作者,全网粉丝合计30w+ ;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板等海量资料。
--End
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。