HashMap不是线程安全的容器,HashTable是同步的容器,但是在有大量线程竞争存在的时会产生严重的低效率的问题,并发容器可以比较好地解决这个问题。
ConcurrentHashMap将数据分为多段,然后每段数据分别有一把锁,当不同的线程分别访问不同的数据段时,申请的是不同的锁,因此不会产生竞争,大大地提高了访问效率。其实就是通过将数据分段,然后采用锁分段技术。
这里有一个问题: 如果将分段的粒度细分为每个键值对,那不是会最大程度地提高并发效率吗?为什么实际上没有这么做?
- Segment的初始化
通过输入的concurrencyLevel参数,确定Segment的个数ssize: ssize是大于等于concurrencyLevel的数中,最小的为2的指数次方的数。
然后根据initialCapacity和ssize计算出每个Segment的初始容量,cap;再和loadFactor一起初始化Segment。对每个Segment实施该操作。
由于增加了数据分段操作,那么在查询时就要先定位segment,再在segment里面查询,相比于普通的HashMap,增加了一个segment的hash定位过程。
比如get方法:
public V get(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).get(key, hash);
}
首先将key的hashCode返回的值再经过一个hash函数,得到再hash值,然后利用这个再hash值去定位segment。
final Segment<K,V> segmentFor(int hash) {
return segments[(hash >>> segmentShift) & segmentMask];
}
segmentShift为32-log2(ssize),也就是取hash值的最高的n位和segmentMask相与,作为segment的index。
然后就是get:
V get(Object key, int hash) {
if (count != 0) { // read-volatile
HashEntry<K,V> e = getFirst(hash);
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v;
return readValueUnderLock(e); // recheck
}
e = e.next;
}
}
return null;
}
这里的count是volatile类型变量。这个get里面不需要加锁,也就是允许多线程的同时读。注意到,这里的count保证了如果同时有线程在写该segment,那么也能保证最新的数据可以反映到本次get中。
最后,如果取出的value值对应为null,表明这个键值对正在put的过程中,那么就加锁后再读一次,保证得到的是完整的数据。
然后是put:
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock();
try {
int c = count;
if (c++ > threshold) // ensure capacity
rehash();
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1);
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue;
if (e != null) {
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value;
}
else {
oldValue = null;
++modCount;
tab[index] = new HashEntry<K,V>(key, hash, first, value);
count = c; // write-volatile
}
return oldValue;
} finally {
unlock();
}
}
可以看到这个put是需要对当前的segment加锁的。
还有remove操作:
V remove(Object key, int hash, Object value) {
lock();
try {
int c = count - 1;
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1);
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue = null;
if (e != null) {
V v = e.value;
if (value == null || value.equals(v)) {
oldValue = v;
// All entries following removed node can stay
// in list, but all preceding ones need to be
// cloned.
++modCount;
HashEntry<K,V> newFirst = e.next;
for (HashEntry<K,V> p = first; p != e; p = p.next)
newFirst = new HashEntry<K,V>(p.key, p.hash,
newFirst, p.value);
tab[index] = newFirst;
count = c; // write-volatile
}
}
return oldValue;
} finally {
unlock();
}
}
注意,因为HashEntry里面的next是final的,不能像常规的直接修改next域,需要复制要删除的Entry前面的节点。并将它们链接起来。
另外,size()方法:
需要统计每个segment里面的count,对每个segment,需要两遍遍历segment,对比其modCount是否发生了变化,相同就返回,否则再遍历一遍,还不相同就需要对所有的segment加锁,再一个个遍历。
在jdk1.8.0中,已经看不到上述代码的踪迹,但是思想应该还是一样的。