JDK8的|JDK8的 CHM 为何放弃分段锁
概述
我们知道, 在 Java 5 之后,JDK 引入了 java.util.concurrent 并发包 ,其中最常用的就是 ConcurrentHashMap 了, 它的原理是引用了内部的 Segment ( ReentrantLock )分段锁,保证在操作不同段 map 的时候, 可以并发执行, 操作同段 map 的时候,进行锁的竞争和等待。从而达到线程安全的目的, 且效率大于 synchronized。
但是在 Java 8 之后, JDK 却弃用了这个分段锁策略,接下来详细学习一下。
文章图片
一、jdk1.7分段锁的实现
和hashmap一样,在jdk1.7中ConcurrentHashMap的底层数据结构是数组加链表。和hashmap不同的是ConcurrentHashMap中存放的数据是一段段的,即由多个Segment(段)组成的。每个Segment中都有着类似于数组加链表的结构。
1.1 关于Segment
ConcurrentHashMap有3个参数:
- initialCapacity:初始总容量,默认16
- loadFactor:加载因子,默认0.75
- concurrencyLevel:并发级别,默认16
1.2 关于分段锁
段Segment继承了重入锁ReentrantLock,有了锁的功能,每个锁控制的是一段,当每个Segment越来越大时,锁的粒度就变得有些大了。
- 分段锁的优势在于保证在操作不同段 map 的时候可以并发执行,操作同段 map 的时候,进行锁的竞争和等待。这相对于直接对整个map同步synchronized是有优势的。
- 缺点在于分成很多段时会比较浪费内存空间(不连续,碎片化); 操作map时竞争同一个分段锁的概率非常小时,分段锁反而会造成更新等操作的长时间等待; 当某个段很大时,分段锁的性能会下降。
二、jdk1.8的map实现 【JDK8的|JDK8的 CHM 为何放弃分段锁】和hashmap一样,jdk 1.8中ConcurrentHashmap采用的底层数据结构为数组+链表+红黑树的形式。数组可以扩容,链表可以转化为红黑树。
2.1 弃用原因
通过JDK 的源码和官方文档看来, 他们认为的弃用分段锁的原因由以下几点:
- 加入多个分段锁浪费内存空间。
- 生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
- 为了提高 GC 的效率.
- 当前容量超过阈值
- 当链表中元素个数超过默认设定(8个),当数组的大小还未超过64的时候,此时进行数组的扩容,如果超过则将链表转化成红黑树
当数组大小已经超过64并且链表中的元素个数超过默认设定(8个)时,将链表转化为红黑树
ConcurrentHashMap的put操作代码如下:
把数组中的每个元素看成一个桶。可以看到大部分都是CAS操作,加锁的部分是对桶的头节点进行加锁,锁粒度很小。
三、为什么不用ReentrantLock而用synchronized ?
- 减少内存开销:如果使用ReentrantLock则需要节点继承AQS来获得同步支持,增加内存开销,而1.8中只有头节点需要进行同步。
- 内部优化:synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。
参考资料
- https://my.oschina.net/pingpangkuangmo/blog/817973
- https://www.wanaright.com/2018/09/30/java10-concurrenthashmap-no-segment-lock/
- https://cloud.tencent.com/developer/article/1509556
推荐阅读
- 不仅仅是一把瑞士军刀|不仅仅是一把瑞士军刀 —— Apifox的野望和不足
- KubeVela: 如何用 100 行代码快速引入 AWS 最受欢迎的 50 种云资源
- 一个数据顾问的成长之路
- 技术平台&应用开发专题月 | 应用多实例调试—开发者的福音
- BFS和DFS的差别|BFS和DFS的差别 BFS实现迷宫最短路径
- Leet|单源点求最短路径的三种常用的方法
- 图论|BFS最短路径的两种打印方法
- 盘点几种常见的java排序算法
- Postman配置多环境请求地址的实现
- java中几种常见的排序算法总结