目录
前言
随着高并发时代的到来,原有的HashMap已经不能满足基本的需求,在HashMap1.7中,多线程下可能出现的的死循环是致命的。但在java api的juc包中有这样一个类:ConcurrentHashMap,它基于HashMap1.7且线程是安全的,本篇博文会仔细对它进行讲解。强烈建议,在阅读本篇博文前,先阅读 HashMap1.7源码详解。
博主默认读者是了解HashMap1.7基本原理的。因此,对于它们相同的部分将不再赘述。在下面所提到的HashMap和ConcurrentHashMap,如无特殊说明,都基于JDK1.7。
HashTable与ConcurrentHashMap
HashTable为保证线程安全付出的代价太大,get()、put()等方法都是synchronized的,这相当于给整个哈希表加了一把大锁。在并发调用HashTable的方法时就会造成大量的时间损耗。
ConcurrentHashMap的设计就显得非常巧妙,它采用分段加锁的方式保证线程安全,而不是将整个哈希表进行加锁,减少了线程阻塞的损耗时间。
数据结构
Segment + HashEntry
每个HashEntry结构都相当于HashMap中的一个哈希表(数组+链表)
ConcurrentHashMap采用分段锁的机制,用Segment来分割整张哈希表;在对不同分段进行操作时,可以做到互不干扰,避免加锁。
Segment类
static final class Segment<K,V> extends ReentrantLock implements Serializable {}
Segment类继承了ReentrantLock类,ReentrantLock和synchronized都是可重入的独占锁,只允许线程互斥的访问临界区,这就验证了ConcurrentHashMap是基于Segment段来加锁的。它实现了Serializable接口,可进行对象的序列化与反序列化。
属性
// 在强制加锁前的最大尝试次数
// availableProcessors()返回java虚拟机的可用处理器数
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
// 每个分段锁里有一个数组
transient volatile HashEntry<K,V>[] table;
// 数组结点数
transient int count;
transient int modCount;
// 扩容阈值
transient int threshold;
// 加载因子
final float loadFactor;
从上面来看,Segment类几乎涵盖了HashMap里的所有核心属性,这也意味着每个Segment对象都相当于一个HashMap,这也就是分段思想的核心。需要强调的是MAX_SCAN_RETRIES和modCount,在ConcurrentHashMap中,一些方法在执行时不是直接加锁,而是通过连续的多次遍历来确定原哈希表是否被别的线程修改了。判断的依据就是modCount是否改变,而循环遍历的次数也不能没有限制,MAX_SCAN_RETRIES就是确定加锁前最大尝试次数的。具体的使用将在代码中进行讲解。
put(K key, int hash, V value, boolean onlyIfAbsent)
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,
// 则返回true,如果获取失败(即锁已被其他线程获取),则返回false,
// 也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
// 取得数组下标
int index = (tab.length - 1) & hash;
// 得到链表首结点
HashEntry<K,V> first = entryAt(tab, index);
// 查找是否有相同的key,若有,则根据onlyIfAbsent来确定是否进行替换
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
// key相同或满足equals条件
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
// onlyIfAbsent: 如果key不存在才增加
if (!onlyIfAbsent) {
e.value = value;
// 修改次数加1
++modCount;
}
break;
}
// 如果当前结点不为空,查询下一结点
e = e.next;
}
// 链表为空,或到了末结点并未找到目标key
else {
// 结点不为空,说明scanAndLockForPut()有返回值
if (node != null)
// 结点前插法,hashmap1.7也用的前插法
node.setNext(first);
// 第一句中tryLock()成功
else
// 前插添加结点,将first作为node的下一结点
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
// 达到扩容阈值且未到最大容量,进行扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
// 将新的头结点组成的链表放到数组中
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
// 保证无论出现任何情况都要解锁
} finally {
unlock();
}
return oldValue;
}
scanAndLockForPut(K key, int hash, V value)
// 方法作用:创建新结点或找到key相同的结点并加锁,为添加做准备
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
// 找到该hashcode对应的链表的头结点
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // 定位节点为负
// 循环获取锁,线程安全
while (!tryLock()) {
HashEntry<K,V> f; // 请在下面重新检查
if (retries < 0) {
// 链表为空或遍历完链表未找到key相同的结点
if (e == null) {
// 在最下面的else if中可能会将retries置为-1
// 所以还可能再次进入这里,需要判断node是否为空
// 这里虽然创建了新的结点,但是并没有链在链表上,e依然为空
if (node == null) // 创建新结点
node