多线程学习之ConcurrentHashMap的原理《六》

 
(1)线程不安全的HashMap
在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所
以在并发情况下不能使用HashMap。例如,执行以下代码会引起死循环。
2. HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表
形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获
取Entry。
3. 效率低下的HashTable
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable
的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同
步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方
法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低
 4. ConcurrentHashMap的锁分段技术可有效提升并发访问率
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的
线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么
当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并
发访问效率,这就是ConcurrentHashMap所使用的 锁分段技术。首先将数据分成一段一段地存
储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数
据也能被其他线程访问。
1. 一个ConcurrentHashMap里包含一个Segment数组
2. 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,
3. 每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,
	必须首先获得与它对应的Segment锁,
1. segments数组的长度ssize是通过concurrencyLevel计算得出的
2. 为了确定segements数组的索引,segments的长度必须是2的N次方,最大时2的16次方,默认是16
比如concurrencyLevel等于14,15,16 其ssize都是16
segmentShift 和segmentMask
1. sshift 等于ssize从1向左移位的次数,默认concurrencyLevel=16,1需要向左移动4位,所以ssift=4
segmentShift = 32-sshift	比如默认sshift=4,segmentShift=28 
segmentMask等于ssize-1,默认是15,最大值是2的16次方-1
1. 输入参数initialCapacity是ConcurrentHashMap的初始化容量
2. 负载因子,loadfactor:0.75(默认)
3. 每一个segment里面的HashEntry数组的长度c是:c=initialCapacity /ssize	
	比如,initialCapacity默认是16,ssize是16 ,那么c=1
	如果C不是1,就会取大于c的2的N次方的值,所以不是1就是大于C的最近的2的N次方值
4. 每一个segement的容量  threshold =int(c)* loadFactor
	默认c=1 loadfactor=0.75 threshold等于0


ConcurrentHashMap 操作

1. get
 1. 整个过程不需要加锁,除非读到null值才会加锁重读
	hashtable get需要加锁,为什么它不用?
2. 原因是它的get方法里将要使用的共享变量都定义成volatile类型,
    如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value.
定义成volatile的变量,能够在线
程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写
(有一种情况可以被多线程写,就是写入的值不依赖于原值),
对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取
volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。
2. PUT
1. put 是写操作 ,为了线程安全必须加锁
2. put  流程
	2.1 先定位segment,,然后在segment里进行插入操作
	2.2 插入前需要2个步骤,第一判断是否需要进行扩容
	2.3 第二定位添加元素的位置,然后放到hashEntry数组里
其实put比这上面要复杂
1. 获取锁,保证put操作的线程安全;
2. 定位到某一个segemnt的HashEntry数组中具体的HashEntry;
3. 遍历HashEntry链表,假若待插入key已存在:
	不存在,更新oldvalue -> newValue,调转步骤5
	否则,直接跳转到步骤5
4.  遍历HashEntry链表,key不存在,插入hashEntry节点,调转步骤5
5. 释放锁,返回oldvalue
3. 是否需要扩容
1. 插入前先判断segment里面的hashEntry数组是否超过这个segment 数组的容量(threshold),超过阈值,扩容
2. 扩容机制比hashmap更好,因为hashmap是插入后判断元素是否已经到达容量
	如果到达了就进行扩容,但是很可能扩容之后没有新元素插入,就说无效的扩容了
3. 与HashMap不同ConcurrentHashMap并不允许key或者value为null
这么扩容?
1. 先创建一个容量是原来容量两倍的数组,然后将原数组元素,进行再散列,放到新数组里面
2. 为了高效,concurrentHashMap并不会对整个容器扩容,而是只对某一个segment进行扩容
4. SIZE
1. 如果要统计整个ConcurrentHashMap里元素的大小,就必须统计所有Segment里元素的大小后求和
2. Segment里的全局变量count是一个volatile变量,那么在多线程场景下,是不是直接把
所有Segment的count相加就可以得到整个ConcurrentHashMap大小了呢?不是的,虽然相加时
可以获取每个Segment的count的最新值,但是可能累加前使用的count发生了变化,那么统计结
果就不准了。
3. 所以最安全的是在统计size的时候把所有Segment的put、remove和clean方法全部锁住,但是这种做法显然非常低效
4. 尝试2次通过不锁住Segment的方式来统计各个Segment大小

因为在累加count操作过程中,之前累加过的count发生变化的几率非常小,所以
ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,
如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。
5. 那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?
使用modCount变量,在put、remove和clean方法里操作元素前都会将变量modCount进行加1,
那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。
 
(1)线程不安全的HashMap
在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所
以在并发情况下不能使用HashMap。例如,执行以下代码会引起死循环。
2. HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表
形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获
取Entry。
3. 效率低下的HashTable
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable
的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同
步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方
法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低
 4. ConcurrentHashMap的锁分段技术可有效提升并发访问率
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的
线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么
当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并
发访问效率,这就是ConcurrentHashMap所使用的 锁分段技术。首先将数据分成一段一段地存
储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数
据也能被其他线程访问。
1. 一个ConcurrentHashMap里包含一个Segment数组
2. 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,
3. 每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,
	必须首先获得与它对应的Segment锁,
1. segments数组的长度ssize是通过concurrencyLevel计算得出的
2. 为了确定segements数组的索引,segments的长度必须是2的N次方,最大时2的16次方,默认是16
比如concurrencyLevel等于14,15,16 其ssize都是16
segmentShift 和segmentMask
1. sshift 等于ssize从1向左移位的次数,默认concurrencyLevel=16,1需要向左移动4位,所以ssift=4
segmentShift = 32-sshift	比如默认sshift=4,segmentShift=28 
segmentMask等于ssize-1,默认是15,最大值是2的16次方-1
1. 输入参数initialCapacity是ConcurrentHashMap的初始化容量
2. 负载因子,loadfactor:0.75(默认)
3. 每一个segment里面的HashEntry数组的长度c是:c=initialCapacity /ssize	
	比如,initialCapacity默认是16,ssize是16 ,那么c=1
	如果C不是1,就会取大于c的2的N次方的值,所以不是1就是大于C的最近的2的N次方值
4. 每一个segement的容量  threshold =int(c)* loadFactor
	默认c=1 loadfactor=0.75 threshold等于0


ConcurrentHashMap 操作

1. get
 1. 整个过程不需要加锁,除非读到null值才会加锁重读
	hashtable get需要加锁,为什么它不用?
2. 原因是它的get方法里将要使用的共享变量都定义成volatile类型,
    如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value.
定义成volatile的变量,能够在线
程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写
(有一种情况可以被多线程写,就是写入的值不依赖于原值),
对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取
volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。
2. PUT
1. put 是写操作 ,为了线程安全必须加锁
2. put  流程
	2.1 先定位segment,,然后在segment里进行插入操作
	2.2 插入前需要2个步骤,第一判断是否需要进行扩容
	2.3 第二定位添加元素的位置,然后放到hashEntry数组里
其实put比这上面要复杂
1. 获取锁,保证put操作的线程安全;
2. 定位到某一个segemnt的HashEntry数组中具体的HashEntry;
3. 遍历HashEntry链表,假若待插入key已存在:
	不存在,更新oldvalue -> newValue,调转步骤5
	否则,直接跳转到步骤5
4.  遍历HashEntry链表,key不存在,插入hashEntry节点,调转步骤5
5. 释放锁,返回oldvalue
3. 是否需要扩容
1. 插入前先判断segment里面的hashEntry数组是否超过这个segment 数组的容量(threshold),超过阈值,扩容
2. 扩容机制比hashmap更好,因为hashmap是插入后判断元素是否已经到达容量
	如果到达了就进行扩容,但是很可能扩容之后没有新元素插入,就说无效的扩容了
3. 与HashMap不同ConcurrentHashMap并不允许key或者value为null
这么扩容?
1. 先创建一个容量是原来容量两倍的数组,然后将原数组元素,进行再散列,放到新数组里面
2. 为了高效,concurrentHashMap并不会对整个容器扩容,而是只对某一个segment进行扩容
4. SIZE
1. 如果要统计整个ConcurrentHashMap里元素的大小,就必须统计所有Segment里元素的大小后求和
2. Segment里的全局变量count是一个volatile变量,那么在多线程场景下,是不是直接把
所有Segment的count相加就可以得到整个ConcurrentHashMap大小了呢?不是的,虽然相加时
可以获取每个Segment的count的最新值,但是可能累加前使用的count发生了变化,那么统计结
果就不准了。
3. 所以最安全的是在统计size的时候把所有Segment的put、remove和clean方法全部锁住,但是这种做法显然非常低效
4. 尝试2次通过不锁住Segment的方式来统计各个Segment大小

因为在累加count操作过程中,之前累加过的count发生变化的几率非常小,所以
ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,
如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。
5. 那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?
使用modCount变量,在put、remove和clean方法里操作元素前都会将变量modCount进行加1,
那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。

猜你喜欢

转载自blog.csdn.net/qq_39455116/article/details/86634508