- ConcurrentHashMap简介
为何要使用ConcurrentHashMap?
Hashtable效率低下,方法阻塞。
HashMap线程不安全,扩容并发情况下get死循环。
ConcurrentHashMap采用分段锁技术提高并发性能。
HashMap和Hashtable比较
①接口继承类
HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary类。都同时实现了Map、Cloneable、Serializable。
②存储key-value
HashMap支持key-value,null-null,key-null,null-value四种形式,Hashtable仅支持key-value(key和value都不为null这种形式),不能使用HashMap的get方法来判断是否存在某个键,而应该用containsKey()方法来判断。get存在null key或者不存在的key都返回null,无法判断是否存在键。
③线程安全性
HashMap非线程安全的,而Hashtable几乎都是synchronized方法。HashMap可以通过synchronizedMap来进行处理,亦或者我们使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
④初始容量和扩充容量
Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
⑤hash散列算法
Hashtable:
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length; //求模
HashMap:
static final int hash(Object key) {
int h;
// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16) 为第二步 高位异或
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}- ConcurrentHashMap的结构
ConcurrentHashMap = Segment + HashEntry。Segment继承了RentrantLock,它也是一种可重入锁,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。ConcurrentHashMap元素定位:
segment定位:(hash >>> segmentShift) & segmentMask
HashEntry定位:hash & (tab.length - 1)
Integer.parseInt("0001111", 2) & 15,(0011111,0111111,1111111),只要低位一致,散列值均为15,冲突严重?
hash值则通过hashcode再散列计算,目的是减少散列冲突,使元素能够均匀地分布在不同的Segment上,从而提高容器的存取效率,计算方法如下
private static int hash(int h) {
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
ConcurrentHashMap扩容:
Segment的扩容判断比HashMap更合理,HashMap在插入元素后再判断是否扩容,如果到达阀值就进行扩容,但是很有可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容。Segment在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阈值,则对数组进行扩容。在扩容的时候,首先会创建一个容量是原来容量两倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里。为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。
ConcurrentHashMap size统计:
先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。
public int size() {
final Segment<K,V>[] segments = this.segments;
int size; // 统计出元素个数
boolean overflow; // 超出Integer。MAX_VALUE
long sum; // 元素变动次数(put、remove和clear导致对应segement的count变化)
long last = 0L; // 存储上一轮元素变动次数
int retries = -1; // 重试统计次数
try {
for (;;) {
if (retries++ == RETRIES_BEFORE_LOCK) { //是否达到2次
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // 达到阀值每段加锁统计
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount; //累加所有segment变动次数
int c = seg.count; //当前segment的元素个数
if (c < 0 || (size += c) < 0) //超出最大值
overflow = true;
}
}
if (sum == last) //上一轮的last值本轮统计sum一致,说明元素没有变动
break; //结束循环
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) { //超出重试统计次数,后需要解锁
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
java并发编程之ConcurrentHashMap
最新推荐文章于 2022-09-05 14:07:35 发布