多线程----ConcurrentHashMap

2022-06-25 10:55:44 浏览数 (1)

学习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是弱一致性的:
  1. 添加元素后不一定能马上读到
  2. 清空后仍可能会有元素
  3. 遍历之前的段元素的变化会读到
  4. 遍历之后的段元素变化读不到
  5. 遍历时元素发生变化不抛异常
三、掌握锁优化的方法。

我们在多线程程序设计过程中可以借鉴什么呢?

  1. 长锁不如短锁,尽量只锁必要部分
  2. 大锁不如小锁,尽可能对加锁的对象进行拆分
  3. 公锁不如私锁,尽可能将锁的逻辑放在私有代码里
  4. 嵌套锁不如扁平锁,尽可能在代码设计时避免嵌套锁
  5. 分离读写锁,尽可能将读锁和写锁分离(如果大部分时间在读,只有少部分时间在写,那么给写加一个大锁,读加volatile或者不加锁)
  6. 粗化高频锁,尽可能合并处理频繁的短锁
  7. 消除无用锁,尽可能不加锁,或用volatile代替(可以保证原子性或者可见性的话)

0 人点赞