
ConcurrentHashMap实现原理
前言
在这篇文章中对属性介绍的比较多:HashMap实现原理
HashMap不是线程安全的,在多线程环境下可以使用Hashtable和ConcurrentHashMap,Hashtable实现线程安全的方式是用synchronized修饰方法,如get和put方法都是用synchronized修饰的,使用的是对象锁,这样会导致线程1get元素(或者put元素)时,线程2不能get元素和put元素,在竞争激烈的时候会出现严重的性能问题
简介
Hashtable出现性能问题的原因是所有访问Hashtable的线程都在竞争一把锁,假如容器中有多把锁,每一把锁用于锁容器的中的一部分数据,那么多线程访问容器里不同数据段的数据时,线程之间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap使用的锁分段技术
ConcurrentHashMap的主要结构如下
假设我们有三个键值对,dnf:1,cf:2,lol:3,每次放值会进行2次hash,即先确定放在哪个Segment中,再确定放在哪个HashEntry中。
假设三个键值对同时进行放,1=hash1(dnf),知道了放在应该放在segments[1]处,接着获取到segments[1]的锁,再进行hash,2=hash2(dnf),即放在hashentrys[2]处,放完对segments[1]解锁
3=hash1(cf),放在segments[3]处,获取到segments[3]的锁,0=hash2(cf),放在hashentrys[0],放完对segments[3]解锁
1=hash1(lol),放在segments[1]处,因为此时segments[1]的锁已经被put key为dnf的线程获取,所以会阻塞的获取锁,直到锁被put key为dnf的这一线程释放,获取到锁后,2=hash2(lol),放在hashentrys[2]处,因为已经有值了,采用头插法,放在链表的头节点
3个线程操作完,结果如下
get方法也是进行两次hash即可,get方法不用上锁,get方法只读不写,用volatile修饰即可
源码
基于jdk1.7.0_80
先看ConcurrentHashMap类的属性
这里说一下RETRIES_BEFORE_LOCK,由于多线程的缘故,调用size和containsValue方法有可能得不到准确的结果 ,不加锁尝试RETRIES_BEFORE_LOCK+1次还得不到准确的结果,直接上锁
接着看Segment内部类
HashEntry内部类
注意这里value和next用volatile修饰保证了可见性
构造方法
默认情况下concurrencyLevel=16,这样就会导致segments的数组长度也是16,每个Segment里面的HashEntry数组的大小为2
接着看put操作
根据索引去segments数组中获取Segment,如果已经存在了则返回,否则创建并自旋插入
ConcurrentHashMap将put操作代理给Segment
将value插入定位到的Segment的HashEntry数组,如果key已经存在,则返回oldValue,否则返回null 注意看最后一个参数,put方法调用的是s.put(key, hash, value, false),即key相等的时候,put会用newValue替换oldValue 而putIfAbsent方法调用的是s.put(key, hash, value, true),即key相等的时候,put不会用newValue替换oldValue
获取锁失败才会调用这个方法,说明锁被其他线程所占有
get方法并没有上锁,而是利用了volatile的可见性
最后看size方法
在计算ConcurrentHashMap的size时,因为并发操作的缘故,还有可能一直插入数据,可能导致计算返回的 size和实际的size有相差(在return size的时候插入了多个数据),因此会分为如下2步来进行
- 尝试不加锁的模式计算2(RETRIES_BEFORE_LOCK)+1次,其中有连续两次计算的总的modCount相等则直接返回size
- 尝试完3次后,如果没有连续两次计算的结果相等,则对segments加锁求size
这里为什么会超过Integer.MAX_VALUE呢?因为ConcurrentHashMap最多有(MAX_SEGMENTS = 2^16)个Segment,而每个Segment允许的最大容量为(MAXIMUM_CAPACITY = 2 30),则最大值为(246),int最大值为(2^ 31 - 1)
文章转载自公众号:Java识堂
