在高并发、多线程场景下,HashMap不安全、HashTable效率低,这时需要在保持同步的同时并发效率比较高,那么,ConcurrentHashMap就自信地出场了...
看一下源码中对这个类的描述:
这个哈希表的主要设计目标是维护并发可读性(通常是get()方法,但也有迭代器和相关方法),同时最小化更新争用。次要目标是保持空间消耗与java.util相同或更好。HashMap,并支持高多个线程对空表的初始插入率。
键值信息存在Node节点中,该节点主要信息为hash、key、value、next指针
/** * Key-value entry. This class is never exported out as a * user-mutable Map.Entry (i.e., one supporting setValue; see * MapEntry below), but can be used for read-only traversals used * in bulk tasks. Subclasses of Node with a negative hash field * are special, and contain null keys and values (but are never * exported). Otherwise, keys and vals are never null. */ static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; Node(int hash, K key, V val, Node<K,V> next) { this.hash = hash; this.key = key; this.val = val; this.next = next; } public final K getKey() { return key; } public final V getValue() { return val; } public final int hashCode() { return key.hashCode() ^ val.hashCode(); } public final String toString(){ return key + "=" + val; } public final V setValue(V value) { throw new UnsupportedOperationException(); } public final boolean equals(Object o) { Object k, v, u; Map.Entry<?,?> e; return ((o instanceof Map.Entry) && (k = (e = (Map.Entry<?,?>)o).getKey()) != null && (v = e.getValue()) != null && (k == key || k.equals(key)) && (v == (u = val) || v.equals(u))); } /** * Virtualized support for map.get(); overridden in subclasses. */ Node<K,V> find(int h, Object k) { Node<K,V> e = this; if (k != null) { do { K ek; if (e.hash == h && ((ek = e.key) == k || (ek != null && k.equals(ek)))) return e; } while ((e = e.next) != null); } return null; } }
ConcurrentHashMap底层实现
ConcurrentHashMap的出现主要为了解决hashmap在并发环境下不安全,JDK1.8ConcurrentHashMap的设计与实现非常精巧,大量的利用了volatile,CAS等乐观锁技术来减少锁竞争对于性能的影响,它保证线程安全的方案是:
-
JDK1.8:volatile+synchronized+CAS+HashEntry+红黑树;
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {}
它的底层数据结构是:数组+链表+红黑树
Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(long(N))),结构基本上与功能和JDK8的HashMap一样,只不过ConcurrentHashMap保证线程安全性。
与JDK1.7相比,它摒弃了分段锁,基于CAS保证数据的获取以及使用synchronized关键字对相应数据段加锁来实现线程安全,这进一步提高了并发性。
因为concurrenthashmap
它们是用于多线程的,并发的 ,如果map.get(key)
得到了null,不能判断到底是映射的value是null,还是因为没有找到对应的key而为空,而用于单线程状态的Hashmap
却可以用containKey(key)
去判断到底是否包含了这个null。
/**
* Tests if the specified object is a key in this table.
*
* @param key possible key
* @return {@code true} if and only if the specified object
* is a key in this table, as determined by the
* {@code equals} method; {@code false} otherwise
* @throws NullPointerException if the specified key is null
*/
public boolean containsKey(Object key) {
return get(key) != null;
}
putVal的操作:
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 初始化表,第一次放数据的时候
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 利用CAS操作将元素插入到Hash表中
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // 数据插入空槽,无需加锁
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
// 哈希冲突,锁定操作,进一步看是链表、树?
V oldVal = null;
synchronized (f) {
// 再次判断是否已被其他线程更新值
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
// 从头节点开始遍历
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// 存在该key,替换旧值
oldVal = e.val;
// 不存在,尾部添加节点
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
// 已树化
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// 执行完synchronized(f)同步代码块之后会检查binCount,如果大于等于TREEIFY_THRESHOLD = 8 则进行treeifyBin操作尝试将该链表转换为红黑树。
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 可能触发扩容 transfer
addCount(1L, binCount);
return null;
}
如果是链表、遍历该链表并统计该链表长度binCount
,查找是否有和key相同的节点,如果有则将查找到节点的val值替换为新的value值,并返回旧的value值,否则根据key,value,hash创建新Node并将其放在链表的尾部
如果Node f
是TreeBin
的类型,则使用红黑树的方式进行插入。然后退出synchronized(f)
锁住的代码块
初始化数组
/** * Initializes table, using the size recorded in sizeCtl. */ private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) Thread.yield(); // 别的线程初始化了,当前线程自旋 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // -1:正在初始化 try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; }
扩容机制(长度小于64时,优先通过扩容解决链表长度过长问题)
往数组里插入数据时,有可能触发扩容动作。
新增节点之后,所在链表的元素个数达到了阈值 8,则会调用
treeifyBin
方法把链表转换成红黑树,不过在结构转换之前,会对数组长度进行判断,数组长度小于64,则会调用tryPresize
方法把数组长度扩大到原来的两倍,并触发transfer
方法,重新调整节点的位置。新增节点之后,会调用
addCount
方法记录元素个数,并检查是否需要进行扩容,当数组元素个数达到阈值时,会触发transfer
方法,重新调整节点的位置。当使用率达到table的阈值时(默认0.75),任何线程注意到这个阈值时会帮助调整和设置替换数组。协助扩容!!!
resize过程中,如果Node节点处理过会被标记为MOVED(hash值-1),别的线程会跳过这个节点处理下一个;有变动去新的table查询!
不考虑多线程情况下的扩容
- 分任务,每个线程不小于16
- 检查nextTable是否为null,如果是则初始化nextTable,使其容量为 table 的两倍
- 循环变量直到 finished,利用 tabAt 方法获得 i 位置的元素(支持多线程复制)
- 如果这个位置为空,就在原table中的 i 位置放入 ForwardingNode 节点,这个也是触发并发扩容的关键点;
- 如果这个位置的 hash 值为 MOVED,表示该位置已经完成了迁移;
- 如果这个位置是Node节点(fh>=0),如果它是一个链表的头节点,就构造一个反序链表,把他们分别放在 nextTable 的 i 和 i+n 的位置上。并将 ForwardingNode 插入原节点位置,代表已经处理过了
- 如果这个位置是 TreeBin 节点(fh<0),也做一个反序处理,并且判断是否需要 unTreeify() 操作,把处理的结果分别放在 nextTable 的 i 和 i+n 的位置上。并插入ForwardingNode 节点
- 遍历过所有的节点以后就完成了复制工作,这时让nextTable作为新的table,并且更新 sizeCtl 为新容量的0.75倍 ,完成扩容。
/** Number of CPUS, to place bounds on some sizings */
static final int NCPU = Runtime.getRuntime().availableProcessors();
/**
* Minimum number of rebinnings per transfer step. Ranges are
* subdivided to allow multiple resizer threads. This value
* serves as a lower bound to avoid resizers encountering
* excessive memory contention. The value should be at least
* DEFAULT_CAPACITY.
*/
private static final int MIN_TRANSFER_STRIDE = 16;
/**
* Moves and/or copies the nodes in each bin to new table.
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// NCPU 操作系统的核数 MIN_TRANSFER_STRIDE 最小扩容时的长度 16 n: 原数组的长度 最小步长16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
帮助扩容:向前寻找需要扩容的节点,当前节点转移完毕设finishing 为 true;
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) { Node<K,V>[] nextTab; int sc; // 判断此时是否仍然在执行扩容,nextTab=null 的时候说明扩容已经结束了 if (tab != null && (f instanceof ForwardingNode) && (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { int rs = resizeStamp(tab.length); while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) {//扩容未完成,不断循环来尝试将当前线程加入到扩容操作中 /** * 扩容结束,直接退出循环 * sc >>> RESIZE_STAMP_SHIFT !=rs, 如果在同一轮扩容中,那么 sc 无符号右移比较高位和 rs 的值,那么应该是相等的。如果不相等,说明扩容结束了 * sc==rs+1 表示扩容结束 * sc=rs+MAX_RESIZERS 表示扩容线程数达到最大扩容线程数 * transferIndex<=0 表示所有的 Node 都已经分配了线程 */ if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || transferIndex <= 0) break; //在低16位 上增加扩容线程数 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { transfer(tab, nextTab);//帮助扩容 break; } } return nextTab; } //返回新的数组 return table; }
/** * Adds to count, and if table is too small and not already * resizing, initiates transfer. If already resizing, helps * perform transfer if work is available. Rechecks occupancy * after a transfer to see if another resize is already needed * because resizings are lagging additions. * * @param x the count to add * @param check if <0, don't check resize, if <= 1 only check if uncontended */ // 大致两类情况: 1、 正在扩容,则当前线程加入协助扩容 2、 无扩容,直接触发扩容 private final void addCount(long x, int check) { CounterCell[] as; long b, s; if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { CounterCell a; long v; int m; boolean uncontended = true; if (as == null || (m = as.length - 1) < 0 || (a = as[ThreadLocalRandom.getProbe() & m]) == null || !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { fullAddCount(x, uncontended); return; } if (check <= 1) return; s = sumCount(); } if (check >= 0) { // 如果 Node<K,V>[] tab, nt; int n, sc; while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { int rs = resizeStamp(n); if (sc < 0) { /** * 以下5个条件任一条件为 true,则当前线程不能帮助进行此次的扩容,跳出循环 * 1. sc >>> RESIZE_STAMP_SHIFT!=rs 表示比较高 RESIZE_STAMP_BITS 位生成戳和 rs 是否相等,判断是否同一次扩容 * 2. sc=rs+1 表示扩容结束 * 3. sc==rs+MAX_RESIZERS 表示帮助线程线程已经达到最大值了 * 4. nt=nextTable -> 表示扩容已经结束 * 5. transferIndex<=0 表示所有的 transfer 任务被领取完,无剩余的 hash 桶来给自己这个线程来扩容 */ if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0) break; if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) transfer(tab, nt); } else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) transfer(tab, null); s = sumCount(); } } }
bound为转移位置减去步长的范围!
nextBound 要与最小步长比较大小、继续转移!
ForwardingNode和synchronized 保证正确性!
- 当一个线程遍历到的节点如果是ForwardingNode,则继续往后遍历。
- 如果不是,则将该节点加锁,防止其他线程进入,完成后设置ForwardingNode节点。
ForwardingNode:一个特殊的Node节点,hash值为-1,其中存储nextTable的引用。 只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或则已经被移动。