随着高并发时代的到来,原有的HashMap已经不能满足基本的需求,在HashMap1.7中,多线程下可能出现的的死循环是致命的。但在java api的juc包中有这样一个类:ConcurrentHashMap,它基于HashMap1.7且线程是安全的,本篇博文会仔细对它进行讲解。强烈建议,在阅读本篇博文前,先阅读 HashMap1.7源码详解。
博主默认读者是了解HashMap1.7基本原理的。因此,对于它们相同的部分将不再赘述。在下面所提到的HashMap和ConcurrentHashMap,如无特殊说明,都基于JDK1.7。
HashTable为保证线程安全付出的代价太大,get()、put()等方法都是synchronized的,这相当于给整个哈希表加了一把大锁。在并发调用HashTable的方法时就会造成大量的时间损耗。
ConcurrentHashMap的设计就显得非常巧妙,它采用分段加锁的方式保证线程安全,而不是将整个哈希表进行加锁,减少了线程阻塞的损耗时间。
Segment + HashEntry
每个HashEntry结构都相当于HashMap中的一个哈希表(数组+链表)
ConcurrentHashMap采用分段锁的机制,用Segment来分割整张哈希表;在对不同分段进行操作时,可以做到互不干扰,避免加锁。
static final class Segment 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[] 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就是确定加锁前最大尝试次数的。具体的使用将在代码中进行讲解。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,
// 则返回true,如果获取失败(即锁已被其他线程获取),则返回false,
// 也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
HashEntry node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry[] tab = table;
// 取得数组下标
int index = (tab.length - 1) & hash;
// 得到链表首结点
HashEntry first = entryAt(tab, index);
// 查找是否有相同的key,若有,则根据onlyIfAbsent来确定是否进行替换
for (HashEntry 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(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 scanAndLockForPut(K key, int hash, V value) {
// 找到该hashcode对应的链表的头结点
HashEntry first = entryForHash(this, hash);
HashEntry e = first;
HashEntry node = null;
int retries = -1; // 定位节点为负
// 循环获取锁,线程安全
while (!tryLock()) {
HashEntry f; // 请在下面重新检查
if (retries < 0) {
// 链表为空或遍历完链表未找到key相同的结点
if (e == null) {
// 在最下面的else if中可能会将retries置为-1
// 所以还可能再次进入这里,需要判断node是否为空
// 这里虽然创建了新的结点,但是并没有链在链表上,e依然为空
if (node == null) // 创建新结点
node = new HashEntry(hash, key, value, null);
retries = 0;
}
// 如果键相同,就不用new新结点
else if (key.equals(e.key))
retries = 0;
// 未到末结点,且不为当前结点,则查询下一个
else
e = e.next;
}
// 下面的代码出现情况:1、new出新结点 2、找到key相同的结点
// 如果扫描次数大于阈值,则强制获取锁
else if (++retries > MAX_SCAN_RETRIES) {
// 超过最大尝试次数就强制加锁,若获取不了锁,则会阻塞(对本Segment对象加锁)
lock();
break;
}
// (retries & 1) == 0,当最低位不为1时成立
// (retries & 1) == 0,没有这一句将可能会造成死循环
// 死循环:如果上面的if总是将retries置为0,而MAX_SCAN_RETRIES >= 1
// 若没有(retries & 1) == 0限制,那下面总是进行重置扫描,即死循环
else if ((retries & 1) == 0 &&
// 如果链表发生变化,代表先于当前线程的线程对链表进行了修改
// 一旦链表关系发生变化,重新遍历查询加锁是必然的
(f = entryForHash(this, hash)) != first) {
e = first = f;
retries = -1;
}
}
return node;
}
// 此方法与上面的方法基本一致,但更高效,用来加锁
private void scanAndLock(Object key, int hash) {
HashEntry first = entryForHash(this, hash);
HashEntry e = first;
int retries = -1;
while (!tryLock()) {
HashEntry f;
if (retries < 0) {
if (e == null || key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f;
retries = -1;
}
}
}
上面这个方法难度是比较大的,读者需要深刻理解每一行代码,有利于后面源码的阅读,它也验证了MAX_SCAN_RETRIES的使用场景。
entryAt(HashEntry
// 使用volatile获取给定表的第i个元素,直接通过偏移地址读取
@SuppressWarnings("unchecked")
static final HashEntry entryAt(HashEntry[] tab, int i) {
return (tab == null) ? null :
// getObjectVolatile()根据tab对象和偏移长度获得对应的属性
// 这里就是获得数组偏移长度的元素,Volatile保证可见性和有序性
(HashEntry) UNSAFE.getObjectVolatile
// TSHIFT:数组每个元素的偏移长度,TBASE:数组首地址的偏移量
// 由这两个参数可以得到该元素的物理地址
(tab, ((long)i << TSHIFT) + TBASE);
}
setEntryAt(HashEntry
// 使用volatile写设置给定表的第i个元素。
static final void setEntryAt(HashEntry[] tab, int i,
HashEntry e) {
// 通过偏移地址将e放到tab的指定位置
UNSAFE.putOrderedObject(tab, ((long)i << TSHIFT) + TBASE, e);
}
关于Unsafe类,本文不进行讲解。
private static final sun.misc.Unsafe UNSAFE;
private static final long SBASE;
private static final int SSHIFT;
private static final long TBASE;
private static final int TSHIFT;
private static final long HASHSEED_OFFSET;
private static final long SEGSHIFT_OFFSET;
private static final long SEGMASK_OFFSET;
private static final long SEGMENTS_OFFSET;
static {
int ss, ts;
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class tc = HashEntry[].class;
Class sc = Segment[].class;
// 数组第一个元素的偏移量,就是首地址
TBASE = UNSAFE.arrayBaseOffset(tc);
SBASE = UNSAFE.arrayBaseOffset(sc);
// 数组中每个元素的大小,对象(指针)大小
ts = UNSAFE.arrayIndexScale(tc);
ss = UNSAFE.arrayIndexScale(sc);
// 获得属性在对象中的偏移地址
HASHSEED_OFFSET = UNSAFE.objectFieldOffset(
ConcurrentHashMap.class.getDeclaredField("hashSeed"));
SEGSHIFT_OFFSET = UNSAFE.objectFieldOffset(
ConcurrentHashMap.class.getDeclaredField("segmentShift"));
SEGMASK_OFFSET = UNSAFE.objectFieldOffset(
ConcurrentHashMap.class.getDeclaredField("segmentMask"));
SEGMENTS_OFFSET = UNSAFE.objectFieldOffset(
ConcurrentHashMap.class.getDeclaredField("segments"));
} catch (Exception e) {
throw new Error(e);
}
// 元素长度必须是2的指数幂,数组元素是对象(指针)
// (ss & (ss-1)) != 0 保证二进制位中只有一个1,其余全为0
if ((ss & (ss-1)) != 0 || (ts & (ts-1)) != 0)
throw new Error("data type scale not a power of two");
// numberOfLeadingZeros(ss),从高位到低位第一个1的0的个数
// 若参数为:0000 0100,那结果为:5
SSHIFT = 31 - Integer.numberOfLeadingZeros(ss);
TSHIFT = 31 - Integer.numberOfLeadingZeros(ts);
}
rehash(HashEntry
// 扩容,扩大数组容量并将旧数组中的结点转移到新数组中
// 本方法在锁中进行,所以线程安全,避免死循环
@SuppressWarnings("unchecked")
private void rehash(HashEntry node) {
HashEntry[] oldTable = table;
int oldCapacity = oldTable.length;
// 在原有容量条件限制下,扩容1倍
int newCapacity = oldCapacity << 1;
// 新的扩容阈值
threshold = (int)(newCapacity * loadFactor);
// 创建新的数组
HashEntry[] newTable =
(HashEntry[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
// 遍历数组,每一条链都进行转移
for (int i = 0; i < oldCapacity ; i++) {
// 得到原数组下标的首结点
HashEntry e = oldTable[i];
// 如果链表不为空,则转移
if (e != null) {
HashEntry next = e.next;
// 重新定位在新数组中的下标
int idx = e.hash & sizeMask;
// 链表只有一个结点
if (next == null)
newTable[idx] = e;
// 如果有多个结点,则将所有的结点都进行转移
else {
// 保存上一个结点
HashEntry lastRun = e;
// 保存上一个结点的下标
int lastIdx = idx;
// 遍历链表
for (HashEntry last = next;
last != null;
last = last.next) {
// 每个结点都需要重定位
int k = last.hash & sizeMask;
// 这里仅对下标做了比较,意味着虽然有多个结点但lastRun一直没变
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
newTable[lastIdx] = lastRun;
// 上面对下标做了标记,所以这里仅仅转移了一条链
for (HashEntry p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
// 重定位
int k = h & sizeMask;
// 前插法
HashEntry n = newTable[k];
newTable[k] = new HashEntry(h, p.key, v, n);
}
}
}
}
// 将新结点加到链表上
int nodeIndex = node.hash & sizeMask;
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
由于rehash()方法的算法奇特,这里需要着重讲解。下面以一条链表为例,做出演示。
原数组某下标中的一条链表,x、y代表重定位后的下标
下面来追踪 lastRun 和 lastIdx 的变化过程
lastRun = Entry0 lastIdx = x
lastRun = Entry1 lastIdx = y
lastRun = Entry2 lastIdx = x
lastRun = Entry4 lastIdx = x
newTable[lastIdx] = lastRun
作用:将最后两个下标为y的结点放到新数组的下标y中
遍历Entry4之前的结点,将它们依次放入新数组中的对应下标中
for (HashEntry p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
// 重定位
int k = h & sizeMask;
// 前插法
HashEntry n = newTable[k];
newTable[k] = new HashEntry(h, p.key, v, n);
}
从上面的演示结果来看,如果一条链表中后面的结点重定位后的下标相同,那就可以将后面下标相同的一条链直接转移,避免了一个结点一个结点的转移;当整条链重定位后都相同时,性能最好。
// 链表删除的基本操作
final V remove(Object key, int hash, Object value) {
if (!tryLock())
// 加锁
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry[] tab = table;
int index = (tab.length - 1) & hash;
// 找到目标下标的首结点
HashEntry e = entryAt(tab, index);
// 前一结点
HashEntry pred = null;
while (e != null) {
K k;
HashEntry next = e.next;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
V v = e.value;
// 如果值为null或值相等
if (value == null || value == v || value.equals(v)) {
// 当前结点为头结点
if (pred == null)
setEntryAt(tab, index, next);
else
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}
与HashMap的Entry作用一致,只是使用了Unsafe来提高读写速度
static final class HashEntry {
final int hash;
final K key;
// 值和结点指向可以改变
volatile V value;
volatile HashEntry next;
HashEntry(int hash, K key, V value, HashEntry next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
// 在本对象的next成员的偏移地址处放入n
final void setNext(HashEntry n) {
UNSAFE.putOrderedObject(this, nextOffset, n);
}
static final sun.misc.Unsafe UNSAFE;
// next成员的偏移地址
static final long nextOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class k = HashEntry.class;
// 得到next成员在对象中的偏移量,用来进行链接操作
// 利用UnSafe类可以通过直接操作内存来提高速度
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 此表的默认并发级别
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
// 每个段表的最小容量
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
// 最大段数,略显保守
static final int MAX_SEGMENTS = 1 << 16;
// 加锁前最大尝试次数,与modCount相配合
static final int RETRIES_BEFORE_LOCK = 2;
// 段的掩码值,通过它进行与运算来定位段下标
final int segmentMask;
// 用来确定哈希值中参与段定位的高位的位数
final int segmentShift;
// 段,每个段都是一个专门的哈希表
final Segment[] segments;
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
// concurrencyLevel:并发水平,即,Segment分段数,不能超过最大段数
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// segmentShift:hash值中段所占的位数
// 若concurrencyLevel = 16,则segmentShift = 27
this.segmentShift = 32 - sshift;
// 段的数量-1,用来通过hash值算目标段的下标
this.segmentMask = ssize - 1;
// 数组大小不能超过最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// c : 每个段需要的哈希表的大小
int c = initialCapacity / ssize;
// 如果initialCapacity / ssize为浮点数,需要向上扩展
if (c * ssize < initialCapacity)
++c;
// 每个段表的数组最小容量为2
int cap = MIN_SEGMENT_TABLE_CAPACITY;
// 扩大得到满足要求的cap
while (cap < c)
cap <<= 1;
// 虽然创建了段的数组,但是只实例化了第一个元素,类似于懒加载
Segment s0 =
new Segment(loadFactor, (int)(cap * loadFactor),
(HashEntry[])new HashEntry[cap]);
Segment[] ss = (Segment[])new Segment[ssize];
// 将s0放到ss数组的第一个元素位置
UNSAFE.putOrderedObject(ss, SBASE, s0);
this.segments = ss;
}
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
}
public ConcurrentHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
public ConcurrentHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
// 以默认的加载因子和同步水平进行初始化
public ConcurrentHashMap(Map extends K, ? extends V> m) {
// threshold = capacity * loadFactor
// -> capacity = m.size / loadFactor
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY),
DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
putAll(m);
}
// put()方法,若存在原始的键值对,则替换值,不存在直接添加
// key、value都不能为null
@SuppressWarnings("unchecked")
public V put(K key, V value) {
Segment s;
// 值不能为null
if (value == null)
throw new NullPointerException();
int hash = hash(key);
// 通过散列值的高n位来确定段下标
int j = (hash >>> segmentShift) & segmentMask;
// 根据物理地址来取得目标段
// SSHIFT :Segment数组每个元素的偏移量,SBASE :Segment数组第一个元素的偏移量
// 由上面两个偏移量可以算出目标下标元素的物理地址
if ((s = (Segment)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
// 确认该段存在,若不存在,该方法会进行初始化
s = ensureSegment(j);
// false : boolean onlyIfAbsent
return s.put(key, hash, value, false);
}
// 若存在原始的键值对,则不替换值,不存在直接添加
@SuppressWarnings("unchecked")
public V putIfAbsent(K key, V value) {
Segment s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j);
// true : boolean onlyIfAbsent
return s.put(key, hash, value, true);
}
// 将目标map的所有映射添加到本对象中
public void putAll(Map extends K, ? extends V> m) {
for (Map.Entry extends K, ? extends V> e : m.entrySet())
put(e.getKey(), e.getValue());
}
ensureSegment(int k)
// 确认段,若没有该段,则创建
private Segment ensureSegment(int k) {
final Segment[] ss = this.segments;
// 获得物理地址,SSHIFT:Segment元素的偏移量,SBASE:ss的第一个元素的地址
long u = (k << SSHIFT) + SBASE;
Segment seg;
// 通过物理地址得到的对象为空时,条件成立
if ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u)) == null) {
// 在构造函数中对segments数组的第一个元素进行了初始化
// 因此第一个元素可以作为模板对其它的空段进行初始化
Segment proto = ss[0];
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
HashEntry[] tab = (HashEntry[])new HashEntry[cap];
// 再次检查该段是否为空,以免别的线程已经创建好了
if ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u))
== null) {
// 创建段对象
Segment s = new Segment(lf, threshold, tab);
while ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u))
== null) {
// cas操作,当目标段为空时,才进行替换,否则不替换
// 这个判断可以防止两个线程同时到这里出现替换两次的情况
// 若一个线程完成了替换,则另一个线程在下一次get时条件不成立退出循环
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
public V get(Object key) {
Segment s;
HashEntry[] tab;
// 得到key的散列值
int h = hash(key);
// h >>> segmentShift : 绝对右移,相当于hash值的高n位参加了段的定位
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
// 如果该下标的段不为空且段中的数组不为空则条件成立
if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
// UNSAFE.getObjectVolatile()得到该数组物理地址下得对象
for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile
// (tab.length - 1) & h : 使用全部散列值进行元素的下标定位
// TSHIFT :哈希表每个元素的偏移量,TBASE :哈希表第一个元素的偏移量
(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;
}
public int size() {
final Segment[] segments = this.segments;
int size;
boolean overflow; // 如果大小溢出32位,则为真
long sum; // modCounts之和
long last = 0L; // 之前的总和
int retries = -1; // 第一次迭代不是重试
try {
for (;;) {
// 加锁前重试次数,RETRIES_BEFORE_LOCK = 2,重试3次
// 当不加锁得不到理想的结果时,强制加锁进行size的统计
if (retries++ == RETRIES_BEFORE_LOCK) {
// 对每一个段都进行加锁
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 seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
// 溢出
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
// 重试,一直到统计期间没有别的线程对哈希表进行操作
// 当遍历两次的sum相同,说明没有别的线程进行干涉,可以返回值
if (sum == last)
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;
}
public V remove(Object key) {
int hash = hash(key);
// 根据散列值取得目标段
Segment s = segmentForHash(hash);
return s == null ? null : s.remove(key, hash, null);
}
public boolean remove(Object key, Object value) {
int hash = hash(key);
Segment s;
return value != null && (s = segmentForHash(hash)) != null &&
s.remove(key, hash, value) != null;
}
本篇博文已将ConcurrentHashMap1.7的关键内部类和方法进行了讲解,通过阅读它的源码,我们发现JDK作者在保证多线程安全性的相关操作上,十分灵活,我们可以从中学习到一些思想,运用到日常的并发编程中。