ConcurrentHashMap,线程安全的HashMap,由于HashTable较重量级,他会给整个加锁,而ConcurrentHashMap只是给每个Segment加锁,所以性能快很多。
除了initialCapacity、loadFactor之外,还有一个concurrentLevel属性,默认情况下,三个属性分别为16,0.75,16
设置以上三个属性后,就得考虑锁加在哪?并怎样初始化加锁的对象?
int sshift = 0;
int ssize = 1;
while(ssize < concurrentLevel){
++sshift;
ssize <<= 1;
}
上面这段代码意思是:计算出一个不小于concurrentLevel的ssize值,而且它是2的n次方。
默认情况下,ssize为16,根据这个参数传入Segment的newArray方法,创建大小为16的Segment数组
创建Segment数组后,数组元素对象怎么初始化?
int c = initialCapacity /ssize
if(c* ssize < initialCapacity){
++c;
}
int cap = 1;
while(cap < c){
cap << 1;
}
上面代码意思是:用Map容量除以Segment数组大小,看每个Segment需要初始化多大,这里16/16=1,所以创建大小为cap=1的HashEntry[]数组,将其赋给Segment,并且基于cap值和loadFactor计算threshold值。Segment继承自ReentrantLock。可以发现。一个Segment的数据结构就相当于HashMap(数组下有链表)
threshold = (int)(newTable.length * loadFactor)
put(key,value)
ConcurrentHashMap并没有对整个方法加锁(而HashTable对整个加锁),和HashMap一样,首先对key.hashCode进行hash操作,得到hash值后计算其对应在数组中的哪个Segment对象。
return segments[(hash >>> segmentShift) & segmentMask]
找到数组中的Segment对象后,接着调用Segment的put方法完成操作,至此,才对其进行加锁:lock,接着判断当前存储的对象个数加1后是否大于threshold,如大于,则rehash,将当前HashEntry[]数组扩大2倍,并重hash对象。
其余的操作跟HashMap差不多,有则覆盖,没有则新创建HashEntry对象,放在链表头部。
HashEntry[] newTable = HashEntry.newArray(oldCapacity<<1)
get(key)
get操作只有在e.value == null的情况下,才会加lock再执行一次e.value
问题:get操作大部分情况没有lock,它是怎样保证并发下数据的一致性的呢?
譬如1:在get找HashEntry链表过程中,这时候可能HashEntry[]数组会发生改变(put操作执行),那它是如何让保证的呢?
答案就是因为HashEntry[]数组是volatile的,当put改变数组后,get操作会立刻得到更新。并且,jdk5以后,volatile语义增强了,不仅仅保证数据的可见性,还能保证禁止在对象上的读写重排序,所以,在get时读取到的HashEntry[]是最新的、并且构造已经完全的
譬如2:当get操作已经找到了HashEntry,准备开始遍历链表了,这时HashEntry发生变化了怎么办?
答案就是HashEntry对象中的hash、key、next都是final的,value是volatile的,这就意味着已获取的HashEntry不会有next加入进来,而且value是可见的。
还有一个问题,为什么要判断e.value是否为null?而且如果为null再调用readValueUnderLock(HashEntry e)?
以下为readValueUnderLock方法:
/**
* Reads value field of an entry under lock. Called if value
* field ever appears to be null. This is possible only if a
* compiler happens to reorder a HashEntry initialization with
* its table assignment, which is legal under memory model
* but is not known to ever occur.
*/
V readValueUnderLock(HashEntry e) {
lock();
try {
return e.value;
} finally {
unlock();
}
}
通过它的注释,我们明白了,This is possible only if a compiler happens to reorder a HashEntry initialization with its table assignment,意思就是,只有在HashEntry初始化时出现指令重排,才会导致该方法调用,并且也不确定是否发生。
所以说,在JDK1.6里面,是弱一致性的,因为所有可见性都是以count实现的,当put和get并发时,get可能获取不到最新的结果。而在1.7里面,会有UNSAFE.getObjectVolatile保证。
网友评论