我是小米,一个喜欢分享技术的29岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!
大家好,我是小米!今天我们来聊聊Java中一个超级实用的线程安全集合类——ConcurrentHashMap。对于多线程环境中需要频繁读写数据的场景来说,ConcurrentHashMap无疑是个好帮手。那么,为什么ConcurrentHashMap效率高?底层实现的奥秘又是什么?接下来,让我们一探究竟。
ConcurrentHashMap与Hashtable的对比在多线程环境中,我们常常需要保证数据的线程安全性。说到实现线程安全,ConcurrentHashMap和Hashtable都是不错的选择,但二者的性能表现却有很大差异。
Hashtable:同步锁的性能瓶颈
Hashtable作为Java早期的线程安全类,主要通过Synchronized关键字进行方法级别的同步来保证线程安全。比如,在执行put或get操作时,Hashtable会锁住整个对象,导致同一时间只能有一个线程访问或修改数据。这样虽然保证了安全性,但性能相对低下。
ConcurrentHashMap:分段锁的高效设计
ConcurrentHashMap的核心思想是分段锁,这使得它在性能上要远优于Hashtable。简单来说,ConcurrentHashMap将数据划分成多个段(Segment),每个Segment对应一个锁。不同线程访问不同Segment的数据时,可以同时进行而不互相阻塞,从而提高了并发性能。
Java的两个主要版本(1.7和1.8)对ConcurrentHashMap的底层结构有很大的差别,我们一起来看看它们的演变过程。
JDK 1.7:Segment分段锁在JDK 1.7中,ConcurrentHashMap使用了分段锁(Segment)的设计。通过这一设计,ConcurrentHashMap达到了提高并发访问率的效果。
底层结构:Segment数组 + HashEntry链表
ConcurrentHashMap在底层将数据分为多个Segment,每个Segment内部由链表存储数据。这样一来,ConcurrentHashMap将整个Map分成了若干个小的子Map,每个Segment相当于一个小的Hashtable,持有一个独立的锁。因此,多个线程访问不同Segment的元素时不会相互影响,从而提高了并发性能。
如何实现分段锁?
ConcurrentHashMap中会对每一个键值对进行哈希计算,以确定它属于哪个Segment。每个Segment锁住一个区域的数据,这样每次只锁定一个Segment,即使一个Segment被锁定,其他Segment也可以同时被访问,这就避免了整个Map锁住的低效情况。
优缺点
优点:提高了并发性能,多个线程可以同时操作不同的Segment。
缺点:Segment数量(默认16个)固定后,无法动态扩容。即使并发再高,也无法突破这个限制。
JDK 1.8:无Segment,链表+红黑树+CASJDK 1.8中,ConcurrentHashMap的底层结构和实现方式发生了重大变化,Segment不再存在,取而代之的是更为精简的实现方式。JDK 1.8摒弃了Segment锁机制,而是采用了数组+链表+红黑树的组合数据结构。
数据结构:Node数组 + 链表/红黑树
JDK 1.8的ConcurrentHashMap与1.8版本的HashMap非常相似,底层通过一个Node数组来存储数据。如果某个桶中有大量hash冲突的数据,会先形成链表;当链表长度超过一定阈值(8)后,会转化成红黑树结构,从而提高查询效率。
并发控制:CAS + synchronized
ConcurrentHashMap 1.8 的线程安全主要通过CAS(Compare And Swap)和synchronized关键字来实现,而不是之前的锁住整个Segment。这样在进行增删改查时,只需要锁住当前操作的链表头部节点即可,大大降低了锁的粒度,进一步提升了并发效率。
CAS机制:CAS在检测到变量未被其他线程修改时,直接更新变量的值。相比传统的锁机制,CAS可以在无锁的情况下完成并发更新,大大提高了效率。
synchronized:当CAS无法保证安全性时,才会退而采用synchronized进行保护。JDK 1.8通过这种灵活的设计,进一步提升了并发性能。
优缺点
优点:性能较JDK 1.7更优,不再依赖Segment;锁的粒度进一步缩小。
缺点:实现较复杂,对内存占用和系统资源提出了更高的要求。
ConcurrentHashMap的核心机制剖析1. get操作
get操作在ConcurrentHashMap中是无锁的,主要通过定位到具体的Node节点来直接获取数据。
流程:
首先通过hash值确定数据的位置。
若找到的桶是链表,则遍历链表寻找对应的节点。
若桶内为红黑树,则使用树的查找逻辑获取目标节点。
2. put操作
在执行put时,ConcurrentHashMap会尝试使用CAS来添加元素。如果当前节点位置为空,CAS更新会成功;否则,系统会退而使用synchronized锁住节点进行更新操作。
流程:
计算key的hash值,定位到具体的桶。
若该位置为空,则使用CAS将新值插入。
若该位置已存在数据:
若为链表,遍历链表并添加至末尾;链表长度超过8则转化为红黑树。
若为红黑树,则按照红黑树的插入规则进行更新。
如果容量超过阈值,则触发扩容。
3. 扩容机制
与HashMap类似,ConcurrentHashMap在容量不足时会进行扩容。不同的是,ConcurrentHashMap的扩容操作是分段进行的。
分段扩容:在扩容过程中,多个线程可以协作进行桶数据迁移,而不是一个线程独自完成扩容,从而减少了线程阻塞。
ConcurrentHashMap的优势总结高并发性能:JDK 1.8后的ConcurrentHashMap通过CAS操作和synchronized,避免了全面锁的低效问题,锁的粒度更小,提高了整体并发性。
高效数据结构:引入红黑树,提升了查询效率,使得冲突严重的情况下,依然能保持较高的访问效率。
分段扩容:扩容过程可由多个线程协作进行,进一步提升了多线程环境下的性能表现。
ENDConcurrentHashMap作为Java中一个重要的并发集合类,凭借其分段锁和CAS机制,在保证线程安全的同时,大大提升了性能。JDK 1.7中通过Segment的分段锁来降低锁竞争,而JDK 1.8中则进一步改进为无锁化操作和红黑树的结构,大幅度提升了性能和并发性。
在实际开发中,如果你需要一个线程安全、高并发的Map集合,ConcurrentHashMap绝对是一个值得信赖的选择!希望今天的分享能够帮助大家更好地理解ConcurrentHashMap的底层设计及其优点,咱们下次再一起探讨更多Java黑科技!