学习ConcurrentHashMap需要达到以下三点:
一、比较HashMap为什么不是线程安全的,及HashTable是如何实现的安全的,并且HashTable有什么问题?
二、深入理解CHM各项并发优化的原理。
三、掌握锁优化的方法。
一、比较HashMap为什么不是线程安全的,及HashTable是如何实现的安全的,并且HashTable有什么问题?
1、HashTable的问题(很暴力):
- 大锁:直接对HashTable对象加锁。
- 长锁:直接对方法加锁。
- 读写锁共用:只有一把锁,从头锁到尾。
CHM对于HashTable的问题进行的优化:
- 小锁:分段锁(5~7),桶节点锁(8)
- 短锁:先尝试获取,失败再加锁
- 分离读写锁:读失败再加锁(5~7),volatile读CAS写(7~8)
二、深入理解CHM各项并发优化的原理。
1、CHM的并发优化历程:
JDK1.5:分段锁,必要时加锁。
如下图对Key进行hash后,高位用来找segment,低位用来找table。
JDK1.6: 优化二次Hash算法。
因为JDK1.5时的hash算法会导致hashcode的高位不均匀分布,对于30000以下的整数key, hash出来后大部分集中在第16个segment中,对于50万以下的整数key,hash出来后大部分集中在第14,15个segment中,这样就退化为了HashTable。而JDK1.6时的hash算法实现高低位的均匀分布。
JDK1.7: 段懒加载(段不一开始就实例化,而是需要时实例化),使用volatile & cas 来避免加锁。
JDK1.7之前的16个segment在一开始全都实例化出来,但是JDK1.7之后需要哪个new哪个。因为segment是需要的时候创建,所以有可能多个线程访问的时候有可见性问题,所以JDK1.7大量的使用了volatile来保证segment的线程安全。
JDK1.8: 摒弃段,基于HashMap原理的并发实现。不必加锁的地方尽量使用volatile,必须加锁的地方如写入,尽量小范围的加锁。
jdk8开始,没有了段,所以只在hash的entry上加锁,缩小了范围。
为何放弃分段锁? 段Segment继承了重入锁ReentrantLock,有了锁的功能,每个锁控制的是一段,当每个Segment越来越大时,锁的粒度就变得有些大了。
2、各版本计数改进:
jdk5~7基于段元素个数求和,二次不同就加锁再求一次。 jkd8没有了段,引入CounterCell, 本质上也是分段计数。
3、CHM是弱一致性的:
- 添加元素后不一定能马上读到
- 清空后仍可能会有元素
- 遍历之前的段元素的变化会读到
- 遍历之后的段元素变化读不到
- 遍历时元素发生变化不抛异常
三、掌握锁优化的方法。
我们在多线程程序设计过程中可以借鉴什么呢?
- 长锁不如短锁,尽量只锁必要部分
- 大锁不如小锁,尽可能对加锁的对象进行拆分
- 公锁不如私锁,尽可能将锁的逻辑放在私有代码里
- 嵌套锁不如扁平锁,尽可能在代码设计时避免嵌套锁
- 分离读写锁,尽可能将读锁和写锁分离(如果大部分时间在读,只有少部分时间在写,那么给写加一个大锁,读加volatile或者不加锁)
- 粗化高频锁,尽可能合并处理频繁的短锁
- 消除无用锁,尽可能不加锁,或用volatile代替(可以保证原子性或者可见性的话)