目录
ConcurrentHashMap与HashMap、HashTable的区别
源码为jdk1.7
ConcurrentHashMap介绍
ConcurrentHashMap 是concurrent包下的一个集合类。它是线程安全的哈希表。它是通过“分段锁”来实现多线程下的安全问题。它将哈希表分成了不同的段内使用了可重入锁(ReentrantLock ),不同线程只在一个段内存在线程的竞争。它不会对整个哈希表加锁。
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
它继承了AbstractMap类,实现了ConcurrentMap,Serializable接口
public abstract class AbstractMap<K,V> implements Map<K,V> {
可以看出AbstractMap类继承了Map接口,也就是说存放着键值对,还有实现了Map接口那些方法。
ConcurrentHashMap底层数据结构
ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构。它内部拥有一个HashEntry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
Segment继承了可重入锁。
static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
this.key = key;
this.hash = hash;
this.next = next;
this.value = value;
}
@SuppressWarnings("unchecked")
static final <K,V> HashEntry<K,V>[] newArray(int i) {
return new HashEntry[i];
}
}
HashEntry这个类有key、value、next、hash四个属性。HashEntry中的value被volatile修饰,这样在多线程读写过程中能够保持它们的可见性。
ConcurrentHashMap部分分析
ConcurrentHashMap具有的属性
//默认容量大小
static final int DEFAULT_INITIAL_CAPACITY = 16;
//默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//默认并发数量(segments数组的大小)
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//最大并发数量
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
static final int RETRIES_BEFORE_LOCK = 2;
final int segmentMask;
final int segmentShift;
//segments数组
final Segment<K,V>[] segments;
//迭代器相关
transient Set<K> keySet;
transient Set<Map.Entry<K,V>> entrySet;
transient Collection<V> values;
构造函数
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
//参数的有效性判断
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
//如果concurrencyLevel大于最大并发量,将其值设为最大并发量。(segments数组的大小)
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
//sshift记录ssize左移的次数
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
segmentShift = 32 - sshift;
segmentMask = ssize - 1;
this.segments = Segment.newArray(ssize);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//c为每个为table数组分配的容量
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = 1;
while (cap < c)
cap <<= 1;
for (int i = 0; i < this.segments.length; ++i)
this.segments[i] = new Segment<K,V>(cap, loadFactor);
}
concurrencyLevel的作用就是用来计算segments数组的容量大小。先计算出“大于或等于concurrencyLevel的最小的2的N次方值”,然后将其保存为“segments的容量大小(ssize)”。
put()方法
public V put(K key, V value) {
if (value == null)
throw new NullPointerException();
int hash = hash(key.hashCode());
return segmentFor(hash).put(key, hash, value, false);
}
如果value为null,抛出异常。不允许值为空。通过key得到hash值。将key-value键值对添加到Segment片段中。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//尝试获得锁,如果失败则通过scanAndLockForPut方法获得锁
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
// 根据”hash值“获取”HashEntry数组中对应的HashEntry链表“
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
//如果key重复,则覆盖旧值
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
//否则创建新的HashEntry节点
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
//如果容量大于阈值,需要重hash
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//解锁
unlock();
}
return oldValue;
}
put()的作用是将key-value键值对插入到“当前Segment对应的HashEntry中”,在插入前它会获取Segment对应的互斥锁,插入后会释放锁。
rehash()的作用是将”Segment的容量“变为”原始的Segment容量的2倍“。
在将原始的数据拷贝到“新的Segment”中后,会将新增加的key-value键值对添加到“新的Segment”中。
get方法
获取key值对应的hash值,在对应的segment里面获取 。
public V get(Object key) {
Segment<K,V> s;
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
可以看到get方法并没有加锁
get(Object key)的作用是返回key在ConcurrentHashMap里的值。首先根据key计算出hash值,获取key对应的Segment片段。如果Segment不为null,则在Segment片段的HashEntry链表中遍历这个链表找到对应的HashEntry节点。
由于遍历过程中其他线程可能对链表结构做了调整,因此get和containsKey返回的可能是过时的数据,这一点是ConcurrentHashMap在弱一致性上的体现。
ConcurrentHashMap与HashMap、HashTable的区别
- ConcurrentHashMap 、HashTable不允许值为null,值为null时抛出异常,HashMap允许值为null
- HashTable所有方法都加锁synchronized,为线程安全的。HashMap不保证线程安全。ConcurrentHashMap采用了分段锁,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。
- ConcurrentHashMap为弱一致性,在get方法时有可能获得过时的数据。