目标:
1.capacity,concurrencyLevel:容量与并发级别的,如何促发扩容,ConcurrentHashMap两个字段的意义:segmentMask,segmentShift
2.jdk1.6/jdk1.7不同之处,jdk1.6
count是volitile的
3.理解ConcurrentHashMap为何支持高并发,对此作了哪些优化,segment, volitile, happens-before, HashEntry final value?
4.记住默认容量、并发级别、加载因子
5.测试不同并发级别下的性能测试,出测试结果
6.实现一个简易的ConcurrentHashMap示例
总结:
1、字段的含义:
1.1 capacity是指整个map的容量,即所有segment中的hashEntry的数,map最终都是在hashEntry的中存储数据的
1.2 concurrencyLevel:并发级别,即ConcurrentHashMap中segment的数量,concurrencyLevel = segmentSize,并发时锁的即是segment,segmentSize 必须是 2^n ,可以参考HashMap的size也是必须是2^n
1.3 segmentShift,segmentMask:segment偏移量,segment掩码,在segmentFor时用到,定位到具体某个segment。segmentMask = segmentSize - 1,segmentShift= 32 - sshift,
sshift=n,n是segmentSize =
2^n中的n
如concurrencyLevel = 16 => sshift=4,segmentMask=15 =>
segmentShift= 32 - 4,再哈希后的数最大是32位二进制数据,所以是32 -
sshift,segmentFor:
hash >>> segmentShift & segmentMask 意思是让高4位参与到hash运算中
2、jdk1.6/jdk1.7: https://my.oschina.net/hosee/blog/675884
最大的区别在于,jdk1.6应用volatile关键字 happens-before原则实现的读/写一直,jdk1.6中用的UNSAFE的原子操作来实现的
3、高并发的原理
3.1 分离锁
无需锁整个ConcurrentHashMap,只在修改结构的时候,只需锁住分段segment
3.2 用 volatile 变量协调读写线程间的内存可见性,使得读(get)可以不加锁
读操作的高效之处在于整个get过程不需要加锁,除非读到的值是空的才会加锁重读。get操作是如何做到不加锁的呢?原因是它的get方法里将要使用的共享变量都定义成volatile,如用于统计当前Segement大小的 count字段和用于存储值的HashEntry的value。定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值),在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁。之所以不会读到过期的值,是根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景(这是JDK1.6的实现)transient
volatile int count;volatile V value;
3.3 用 HashEntery 对象的不变性来降低读操作对加锁的需求
HashEntry 中的 key,hash,next 都声明为 final。这意味着,不能把节点添加到链接的中间和尾部,也不能在链接的中间和尾部删除节点。这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变。这个特性可以大大降低处理链表时的复杂性。
3.4 总结
基于通常情形而优化:
在实际的应用中,散列表一般的应用场景是:除了少数插入操作和删除操作外,绝大多数都是读取操作,而且读操作在大多数时候都是成功的。正是基于这个前提,ConcurrentHashMap 针对读操作做了大量的优化。通过 HashEntry 对象的不变性和用 volatile型变量协调线程间的内存可见性,使得大多数时候,读操作不需要加锁就可以正确获得值。这两个特性相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 , 读线程才需要加锁后重读)
ConcurrentHashMap 的高并发性主要来自于三个方面:
用分离锁实现多个线程间的更深层次的共享访问。
用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
通过对同一个Volatile 变量的写 / 读访问,协调不同线程间读/ 写操作的内存可见性
4、默认容量:
DEFAULT_INITIAL_CAPACITY
= 16;
DEFAULT_CONCURRENCY_LEVEL
= 16;
DEFAULT_LOAD_FACTOR = 0.75f;
MAXIMUM_CAPACITY
= 1 << 30;
MAX_SEGMENTS =
1 << 16;
5、?Google ConcurrentHashMapperformancetest
6、?
参考:
Jdk1.6
http://www.infoq.com/cn/articles/ConcurrentHashMap/
https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/
jdk1.7
网友评论