ConcurrentHashMap总结
其实我是不想写的,很多人已经写的很详细了,自己再写有些多次一举。这里只是做个总结。
JDK1.6,JDK1.7,JDK1.8的实现细节都有所不同,下面以JDK1.7代码为主,
ConcurrentHashmap和HashTable的区别:
- HashTable依赖synchronized关键字实现线程安全的,但是synchronized的性能十分低下,对get操作都要做加锁操作,使用的是全局锁,相当于数据库中使用的表锁,锁的颗粒度越大,并发效果越差,但是安全性高;
- ConcurrentHashmap引入了分段锁的概念,对不同的数据加不同的锁,相当于数据库中的行锁,锁的颗粒度变小,并发效果变好,但是ConcurrentHashmap带来的是一致性是弱一致性的,如果对一致性要求非常高,还是要是用HashTable或者Collections提供的synchronizedHashMap。
盗图一张
两个内部类来实现
HashEntry类
static final class HashEntry<K, V> {
final int hash;
final K key;
volatile V value;
volatile ConcurrentHashMap.HashEntry<K, V> next;
}
Segment类
static final class Segment<K, V> extends ReentrantLock implements Serializable {
transient volatile ConcurrentHashMap.HashEntry<K, V>[] table;
transient int threshold;
final float loadFactor;
Segment(float lf, int threshold, ConcurrentHashMap.HashEntry<K, V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
}
HashEntry用来保存键值对,Segment继承自ReentrantLock,在这里充当锁。
定位
源代码的一些位运算我真没看懂,思路是:
hash算法获取hash值,然后和segment长度减1(初始值是16,减1就是15)做与操作,得到对应的segment,然后再次hash计算对应的桶的位置。也就是做了两次hash算法,双hash算法是为了确保数据尽可能的分散,一次hash算法有很大的可能发生冲突。
get操作
先计算key的hash值,然后使用这个哈希值定位到segment,再通过哈希算法定位到元素,get的操作是很高效的,原因在于不需要加锁,不加锁而不会发生并发的原因在于:
volatile V value;
volatile ConcurrentHashMap.HashEntry<K, V> next;
- volatiles保持可见性;
- jvm内存模型happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。;
- 通过Unsafe对象的getObjectVolatile()方法提供的原子读语义,来获得Segment以及对应的链表,但由于遍历过程中其他线程可能对链表结构做了调整,因此get和containsKey返回的可能是过时的数据,这一点是ConcurrentHashMap在弱一致性上的体现。如果要求强一致性,那么必须使用Collections.synchronizedMap()方法。
put操作
put操作是加锁了,所以在安全性上没有问题的,但怎么保证在put的时候,可以保证get的正确性呢?JDK1.7使用了Unsafe的原子性操作保持内存的可见性。
remove操作
这个和put类似了
size操作
这是基于整个ConcurrentHashMap来进行操作的,原理是:首先不加锁循环执行以下操作:循环所有的Segment(通过Unsafe的getObjectVolatile()以保证原子读语义),获得对应的值以及所有Segment的modcount之和。如果连续两次所有Segment的modcount和相等,则过程中没有发生其他线程修改ConcurrentHashMap的情况,返回获得的值。(这个和CAS原理是一样的)
总结
在使用锁协调多线程环境下同步访问时,减少对锁的竞争可以有效提高并发度,减少对锁的竞争有两种方法:
- 减少请求锁的次数
- 减少持有锁的时间
ConcurrentHashMap中通过以下三中措施提高并发性能:
- 分段锁Segment的使用可以有效减少发生竞争的可能性
- HashEntry中value和next域的volatile属性可以减少持有锁的时间
- HashEntry倒序更新的方式可以减少访问次数和持有锁的时间