1.7版本
它维护了一个 segment 数组,每个 segment 对应一把锁
- 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 jdk8 中是类似的(jdk8中是把锁加在链表头上,jdk7是把锁加在segment对象上)
- 缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化(构造方法一执行就会创建需要用到的数组)
构造器分析
// 默认传入的initialCapacity为16(初始容量,即所有Segment数组存储键值对数量的总和)
// loadFactor为0.75
// concurrencyLevel为16(并发度,即Segment数组大小)
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
int sshift = 0;
int ssize = 1;
// sshift是ssize从1向左移位的次数。默认情况下,concurrencyLevel是16,则sshift是4
// ssize 必须是 2^n, 即 2, 4, 8, 16 ... 表示了 segments 数组的大小,默认为16
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// segmentShift 默认是 32 - 4 = 28
// this.segmentShift 和 this.segmentMask 的作用是决定将 key 的 hash 结果匹配到哪个 segment
this.segmentShift = 32 - sshift;
// segmentMask是散列运算的掩码,segmentMask = 并发度 - 1,默认为15 即 0000 0000 0000 1111
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 根据initialCapacity计算Segment数组中每个位置可以分到的大小,向上取整
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
// 默认MIN_SEGMENT_TABLE_CAPACITY是2,插入一个元素不至于扩容,插入第二个的时候才会扩容
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// 创建 segments and segments[0](segments[0]存的是一个HashEntry数组)
// 这里说明了创建数组过程中和jdk8的不同,此处是直接创建需要用到的数组,
// 而不是jdk8的懒加载
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
// 往数组第一个位置写入s0
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
构造完成,如下图所示
可以看到 ConcurrentHashMap 没有实现懒惰初始化,空间占用不友好
其中 this.segmentShift 和 this.segmentMask 的作用是决定将 key 的 hash 结果匹配到哪个 segment
例如,根据某一 hash 值求 segment 位置,先将该hash值从高位向低位移动 this.segmentShift 位
put() 方法
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
// 计算出hash
int hash = hash(key);
// 计算出 segment 下标,进行移位运算之后再进行与运算
// 我们前面已经讲到,在默认情况下,segmentShift为28,
// segmentMask为15,hash向右无符号移动28位,
// 是为了能够让高四位参与到散列运算中。
int j = (hash >>> segmentShift) & segmentMask;
// 获得 segment 对象, 判断是否为 null, 是则创建该 segment
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null) {
// 这时不能确定是否真的为 null, 因为其它线程也可能发现该 segment 为 null
// 因此在 ensureSegment 里用 cas 方式保证该 segment 安全性
// ensureSegment中将创建Segment对象
s = ensureSegment(j);
}
// concurrentHashMap实际上调用的是segment的put方法,进入 segment 的put 流程
return s.put(key, hash, value, false);
}
// segment 继承了可重入锁(ReentrantLock),它的 put 方法为
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// tryLock()尝试加锁,tryLock()方法会立刻返回一个true或者false
HashEntry<K,V> node = tryLock() ? null :
// 如果不成功, 进入 scanAndLockForPut 流程
// 如果是多核 cpu 最多 tryLock 64 次, 进入 lock 流程
// 在尝试期间, 还可以顺便看该节点在链表中有没有, 如果没有顺便创建出来
scanAndLockForPut(key, hash, value);
// 执行到这里 segment 已经被成功加锁, 可以安全执行
V oldValue;
try {
// HashEntry数组相当于一个小的hash表
HashEntry<K,V>[] tab = table;
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 {
// 1) 如果之前等待锁时, node 已经被创建, next 指向链表头
if (node != null)
node.setNext(first);
else
// 2) 创建新 node
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
// 3) 扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
// 注意并没有先插入再扩容,而是把将要插入的节点的next指针指向当前链表头结点
// 随后就开始扩容
rehash(node);
else
// 将 node 作为链表头,这里看到使用的是头插法
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
scanAndLockForPut方法
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
// 根据哈希值在当前Segment所对应的HashEntry数组
// 中找到数组对应的HashEntry
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
// 再次尝试获取当前Segment锁,注意,每一个Segment本身就是一个ReentrantLock
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
// 如果第一次进入循环体,并且发现当前位置的HashEntry为null,还没放过数据
// 则把当前数据封装为HashEntry
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
// 这里是退出循环的逻辑,如果是多核CPU
// 表示需要循环64次retries才能进入这里边
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
// 如果在偶数次进入循环体时
// 当前数据hash值对应的元素与之前创建的元素不一样(发生了其他元素插入)
// 那么需要重新进入第一轮循环的流程
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
// 一旦成功获取锁,则把封装的HashEntry返回出去
return node;
}
ensureSegment()方法
// 根据传来的Segment数组下标创建对应位置的Segment实例
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
// 使用segment[0]当做原型,根据其数组长度和负载因子来初始化其他segment
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
// 初始化segment[k]内部的数组
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
// 使用while循环,内部用CAS,当前线程成功设值或其他线程成功设值后退出
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
rehash() 方法
发生在 put 中,因为此时已经获得了锁,因此 rehash 时不需要考虑线程安全。在插入元素前会先判断HashEntry数组是否超过容量,如果是的话,则要进行扩容,segment的扩容操作比HashMap的扩容操作更加恰当,因为HashMap是在插入元素之后判断是否需要扩容,但是很有可能扩容之后没有新元素插入,那这时HashMap就进行了一次无效的扩容。
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
// 新容量为旧容量的两倍
int newCapacity = oldCapacity << 1;
threshold = (int)(newCapacity * loadFactor);
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
for (int i = 0; i < oldCapacity ; i++) {
// 找到每一条链表
HashEntry<K,V> e = oldTable[i];
// 如果链表中有元素
if (e != null) {
HashEntry<K,V> next = e.next;
// 求出第一个元素在扩容后的数组中的下标
int idx = e.hash & sizeMask;
// 1. Single node on list,如果只有一个节点,那么就直接移动到新数组中的合适位置
if (next == null)
newTable[idx] = e;
// 2. 多个节点: Reuse consecutive sequence at same slot 在同一插槽中重复使用连续序列
else {
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
// 2.1 过一遍链表, 尽可能把 rehash 后 idx 不变的节点重用(即尽可能进行搬迁工作而不是重建)
//下面这个for循环会找到一个lastRun节点,这个节点之后的所有元素是将要放到一起的
for (HashEntry<K,V> last = next; last != null; last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
// 将lastRun及其之后的所有节点组成的这个链表放到lastIdx这个位置
newTable[lastIdx] = lastRun;
// 下面的操作是处理lastRun之前的节点,
// 这些节点可能分配在另一个链表中,也可能分配到上面的那个链表中
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
// 再一次看出是头结点插入
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
// add the new node 扩容完成, 才加入新的节点
int nodeIndex = node.hash & sizeMask;
node.setNext(newTable[nodeIndex]);
// 将新节点设置为链表头
newTable[nodeIndex] = node;
// 替换为新的 HashEntry table
table = newTable;
}
get() 方法
get 时并未加锁,用了 UNSAFE 方法保证了可见性,扩容过程中,get 先发生就从旧表取内容,get 后发生就从新
表取内容
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
// u 为 segment 对象在数组中的偏移量
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
// 1.获取到Segment,s 即为 segment
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
// 2.获取到Segment中的HashEntry
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;
}
size() 方法
如果要统计整个ConcurrentHashMap里元素的数目,就必须统计所有segment里元素的数目,然后求和。Segment类中有一个count变量来记录对应segment里元素的数目,那是不是直接相加所有segment的count就可以了呢?不是的,我们在求和的时候,有可能有segment中元素的数目发生了变化,导致结果不准确。所以,最安全的做法就是在统计时,把所有segment的put、remove和clean方法都锁住,但是这种做法显然非常低效。
因为在累加count的过程中,count发生变化的概率比较小,所以ConcurrentHashMap的做法是先尝试2次通过不加锁的方式来计算各个segment的大小,若统计过程中,容器的count发生了变化,则再采用加锁的方式来统计所有segment的大小。
那ConcurrentHashMap是怎么判断在统计过程中容器是否发生变化呢?答案就是使用modCount变量,在put、remove、clean方法里操作元素前都会将modCount加1,那么在统计size前后比较modCount是否发生变化,就可以知道容器大小是否发生变化。
- 计算元素个数前,先不加锁计算两次,如果前后两次结果如一样,认为个数正确返回
- 如果不一样,进行重试,重试次数超过 3,将所有 segment 锁住,重新计算个数返回
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
// 重试次数是否达到了RETRIES_BEFORE_LOCK,RETRIES_BEFORE_LOCK = 2
if (retries++ == RETRIES_BEFORE_LOCK) {
// 超过重试次数, 需要创建所有 segment 并加锁
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
// modCount表示最近修改的次数,比如put
sum += seg.modCount;
// count表示每个segment中元素的个数
int c = seg.count;
if (c < 0 || (size += c) < 0)
// 表示已经溢出了
overflow = true;
}
}
// 如果sum == last,那么说明这段区间没有其它线程干扰
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;
}
1.8版本
我们对JDK1.8版本的ConcurrentHashMap进行说明,1.8版本的ConcurrentHashMap相比之前的版本主要做了两处改进:
- 使用CAS代替分段锁。
- 红黑树,这一点和HashMap是一致的。
ConcurrentHashMap有如下五个构造方法:
// table默认大小为16
public ConcurrentHashMap() {
}
// 初始化容量为 >= 1.5 * initialCapacity + 1计算出的2的整数次幂
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
// 创建一个和输入参数map映射一样的map
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
// 根据initialCapacity和loadFactor来计算size
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
// 初始化容量应该为不小于size的2的整数次幂
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
在ConcurrentHashMap的构造方法中,并没有初始化table(除了第三个构造方法,调用了putAll来初始化),table的初始化发生在第一次插入操作,默认大小为16的数组,在ConcurrentHashMap中,元素都被封装为了Node对象:
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)));
}
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;
}
}
红黑树TreeNode节点类型
static final class TreeNode<K,V> extends Node<K,V> {
// 父节点
TreeNode<K,V> parent; // red-black tree links
// 左子节点
TreeNode<K,V> left;
// 右子节点
TreeNode<K,V> right;
// 删除节点时,需要断开链接,这个节点记录了删除节点的前一个节点
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next,
TreeNode<K,V> parent) {
super(hash, key, val, next);
this.parent = parent;
}
Node<K,V> find(int h, Object k) {
return findTreeNode(h, k, null);
}
final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
if (k != null) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk; TreeNode<K,V> q;
TreeNode<K,V> pl = p.left, pr = p.right;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.findTreeNode(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
}
return null;
}
}
关键的静态属性
// 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认容量16
private static final int DEFAULT_CAPACITY = 16;
// 数组最大长度,在toArray等相关方法中用到
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 默认并发级别,已经不再使用的字段,之所以还存在只是为了和之前的版本兼容
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 此表的加载因子。在构造函数中重写此值只会影响初始表的容量,而不会使用实际的浮点值
private static final float LOAD_FACTOR = 0.75f;
// 添加当前元素,bin中元素个数若为8,则将链表转为红黑树
static final int TREEIFY_THRESHOLD = 8;
// bin中元素个数若为6个,则将红黑树转为链表
static final int UNTREEIFY_THRESHOLD = 6;
// table转为红黑树的阈值,此值最小为4*TREEIFY_THRESHOLD,默认为64
static final int MIN_TREEIFY_CAPACITY = 64;
// table扩容时,bin转移个数,最小为默认的DEFAULT_CAPACITY=16
// 因为扩容时,可以多个线程同时操作,所以16个bin会被分配给多个的线程进行转移
private static final int MIN_TRANSFER_STRIDE = 16;
// 用来控制扩容时,单线程进入的变量
private static int RESIZE_STAMP_BITS = 16;
// resize时的线程最大个数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 用来控制扩容,单线程进入的变量
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// 节点hash域的编码
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
// 当前可用cpu数量
static final int NCPU = Runtime.getRuntime().availableProcessors()
很多静态变量都与HashMap中的变量相似。同时,ConcurrentHashMap还有如下几个成员变量:
// Node数组,该变量只有在第一次插入元素时才会初始化
transient volatile Node<K,V>[] table;
// resize时用到的临时table,只有在resize时才不为null
private transient volatile Node<K,V>[] nextTable;
// 基本计数器值,主要用于没有争用时,也可作为表初始化期间的后备,通过CAS更新。
private transient volatile long baseCount;
/**
* 用于控制table初始化和resize的一个变量
* 值为负数:table正在初始化or正在resize
* sizeCtl = -1:正在初始化;
* sizeCtl = -(1 + n):当前有n个线程正在进行resize;
* 当table未初始化时,保存创建时使用的初始表大小,或默认为0
* 初始化后,保存下一个要调整table大小的元素计数值
*/
private transient volatile int sizeCtl;
// resize时,next table的索引+1,用于分割
private transient volatile int transferIndex;
//在调整大小和/或创建CounterCells时使用的自旋锁(通过CAS锁定)
private transient volatile int cellsBusy;
//这是一个计数器数组,用于保存table中每一下标对应的节点个数
private transient volatile CounterCell[] counterCells;
put
最核心的便是put方法:
public V put(K key, V value) {
return putVal(key, value, false);
}
最后一个参数为onlyIfAbsent,表示只有在key对应的value不存在时才将value加入,所以putVal是put和putIfAbsent两个方法的真正实现。
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 若key或value为null,则直接抛出NullPointerException异常
if (key == null || value == null) throw new NullPointerException();
// 计算节点的hash值
int hash = spread(key.hashCode());
// 用来记录相应链表的长度
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果table数组为null或长度为0,则对数组进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 否则,按照hash值对应的数组下标,得到第一个节点f
// 若f为null,则通过CAS将该节点设置为对应下标的首节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 若果f节点的hash值为MOVED,此时表示数组在扩容,则帮助数据迁移
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//若当前槽位已经有元素了,则加载链表尾端
else {
V oldVal = null;
// 获取数组该位置的头结点的监视器锁
synchronized (f) {
if (tabAt(tab, i) == f) {
// 头结点的hash值大于0,说明是链表,因为红黑树的根节点hash值是TREEBIN(-2)
if (fh >= 0) {
// 用于累加,记录链表的长度
binCount = 1;
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 如果发现了"相等"的key,判断是否要进行值覆盖
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
// put方法传入的onlyIfAbsent默认为false,即可以覆盖
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;
// 调用红黑树的putTreeVal方法插入新节点
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// 插入节点后,检查数组对应下标的节点个数是否 >= TREEIFY_THRESHOLD,如果是,则由链表转为红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 更新元素数目
addCount(1L, binCount);
return null;
put方法的主流程看完了,但是putVal方法中还调用到了一些其他方法,我们来看一下。首先是计算节点hash值的spread(int)方法
static final int spread(int h) {
// HASH_BITS = 0x7fffffff
return (h ^ (h >>> 16)) & HASH_BITS;
}
为什么采用h ^ (h >>> 16)的方式来计算hash值,前面我们介绍HashMap时,已经做了解释,这里不再叙述。计算出来的hash值还要和HASH_BITS进行与运算才是最终结果,为什么要进行与运算呢?HASH_BITS的值是0x7fffffff,一个整形数字与HASH_BITS进行与运算,其实就是将数字二进制表示的第一位设置为0,它这样做的目的是消除符号位的影响,因为在table中,有些节点的hash值是特定的负数,比如前面介绍到的节点的hash域编码:
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
初始化数组(initTable)
如果table为空或大小为0,那么将对其进行初始化操作,我们来看源代码:
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 若table为null或者table的长度为0,则进行初始化操作
while ((tab = table) == null || tab.length == 0) {
// 若我们设置了初始容量,则在构造方法会设置sizeCtl的值,否则,sizeCtl为0
// 若sizeCtl小于0,则表明已经有其他线程在初始化
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
// 通过CAS操作将sizeCtl设置为-1,代表本线程来初始化,其他线程就不要初始化了
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// 获取table的初始容量
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
// 初始化数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// 计算sc的值,其实sc = 0.75 * n
sc = n - (n >>> 2);
}
} finally {
// 设置sizeCtl为sc,即下一次需要扩容数组时的元素数量阈值
sizeCtl = sc;
}
break;
}
}
return tab;
}
sizeCtl是ConcurrentHashMap的初始化,扩容操作中一个至关重要的控制变量,其声明:
private transient volatile int sizeCtl;
其取值可能为:
-
0: 初始值。
-
-1: 正在进行初始化。
-
负值(小于-1): 表示正在进行扩容,因为ConcurrentHashMap支持多线程并行扩容。
-
正数: 表示下一次触发扩容的临界值大小,即当前值 * 0.75(负载因子)。
从源码中可以看出,ConcurrentHashMap只允许一个线程进行初始化操作,当其它线程竞争失败(sizeCtl < 0)时便会进行自旋,直到竞争成功(初始化)线程完成初始化,那么此时table便不再为null,也就退出了while循环。
Thread.yield方法用于提示CPU可以放弃当前线程的执行,当然这只是一个提示(hint),这里对此方法的调用是一个优化手段。
对SIZECTL字段CAS更新的成功便标志者线程赢得了竞争,可以进行初始化工作了,剩下的就是一个数组的构造过程,一目了然。
转为红黑树(treeifyBin)
指putVal源码中的:
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
注意,这段代码是在上述(节点添加部分)同步代码块之外执行的。
TREEIFY_THRESHOLD表示将链表转为红黑树的链表长度的临界值,默认为8.
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
// MIN_TREEIFY_CAPACITY = 64
// 如果数组长度小于64,则会进行数组扩容
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
// 扩容
tryPresize(n << 1);
// 否则,b为index对应链表的首节点
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
// 获取b的监视器锁
synchronized (b) {
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
// 下面就是遍历链表,建立一颗红黑树
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
// 将红黑树设置到数组相应位置中
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
从源码中我们知道,treeifyBin方法不一定就会进行红黑树转换,也可能是仅仅做数组扩容。扩容是通过tryPresize(int)方法来完成的,int参数就是扩容或的值,我们下面来看扩容操作。
扩容(tryPresize)
如果当前bin的个数未达到MIN_TREEIFY_CAPACITY,那么不再转为红黑树,转而进行扩容。MIN_TREEIFY_CAPACITY默认64
// 首先要说明的是,方法参数size传进来的时候就已经翻倍了
private final void tryPresize(int size) {
// 尝试将table大小设定为1.5 * size + 1,以容纳元素
// tableSizeFor方法将传入数值处理为2的倍数返回
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
// 若sizeCtl < 0,则表明已经有其他线程在扩容
// 若sizeCtl >= 0,则本线程进行扩容
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
// 若table为null或者table的长度为0,则进行初始化操作
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
// 通过CAS操作将sizeCtl设置为-1,代表本线程来初始化,其他线程就不要初始化了
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
// 创建新数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
// 计算sc的值,其实sc = 0.75 * n
sc = n - (n >>> 2);
}
} finally {
// 设置sizeCtl为sc
sizeCtl = sc;
}
}
}
// 若扩容值小于原阀值,或现有容量 >= 最大值,则直接退出
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
// table不为空,且在此期间,其他线程没有修改table
else if (tab == table) {
// 返回table的扩容标记位
int rs = resizeStamp(n);
// 已经有线程在进行扩容工作
if (sc < 0) {
Node<K,V>[] nt;
// 条件1检查原容量为n的情况下进行扩容,保证sizeCtl与n是一块修改好的,
// 条件2与条件3在当前RESIZE_STAMP_BITS情况下应该不会成功。
// 条件4与条件5确保tranfer()中的nextTable相关初始化逻辑已走完。
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 有新线程参与扩容则sizeCtl加1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 修改sizeCtl,扩容时为-(n+1),n为当前扩容线程数
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
前面提到过了,ConcurrentHashMap支持多线程并行扩容,具体来说,是支持多线程将节点从老的数组拷贝到新的数组,而新数组创建仍是一个线程完成(不然多个线程创建多个对象,最后只使用一个,这不是浪费是什么?)
竞争成功的线程为transfer方法的nextTab参数传入null,这将导致新数组的创建。竞争失败的线程将会判断当前节点转移工作是否已经完成,如果已经完成,那么意味着扩容的完成,退出即可,如果没有完成,那么此线程将会进行辅助转移。
判断是否已经完成的条件只能理解(nt = nextTable) == null || transferIndex <= 0两个。
我们看到tryPresize方法中调用了一个叫做resizeStamp的方法,我们看一看这个方法做了什么事情:
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
这个方法会返回一个与table容量n大小有关的扩容标记,它的实现很简单,Integer.numberOfLeadingZeros(int n)方法是计算在n的二进制表示中,高位一共有多少个连续的0,如果是负数则返回0。然后将其与1 << (RESIZE_STAMP_BITS - 1)进行或运算。它这么做的意义是什么呢?我写了一段代码来验证该方法的作用
private static int RESIZE_STAMP_BITS = 16;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
public static void main(String[] args) throws UnsupportedEncodingException {
int size = 1 << 4;
int rs = resizeStamp(size);
formatPrintNum(rs);
int rs2 = (rs << RESIZE_STAMP_SHIFT) + 2;
formatPrintNum(rs2);
formatPrintNum(rs2 + 1);
}
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
static void formatPrintNum(int n) {
String s = Integer.toBinaryString(n);
while (s.length() < 32) {
s = "0" + s;
}
s = s.substring(0, 16) + " | " + s.substring(16, 32);
System.out.println(s);
}
运行结果:
0000000000000000 | 1000000000011011
1000000000011011 | 0000000000000010
1000000000011011 | 0000000000000011
我把结果的输出分为了高16位和低16位。
假设table的容量为16,则通过resizeStamp方法计算出的扩容标记位是“1000000000011011”(只看低16位),(rs << RESIZE_STAMP_SHIFT) + 2(后面称为rs2)的值是“1000000000011011 | 0000000000000010”,rs2+1的值是“1000000000011011 | 0000000000000011”。
这个程序的目的是什么呢?
我们先来看rs2,rs2的值一直为负数,因为resizeStamp方法中(1 << (RESIZE_STAMP_BITS - 1))计算出来的值二进制表示为“1000000000000000”(低16位),通过或运算后得出的扩容标记位rs的二进制表示中,第16位一定为1,而rs2是通过rs左移RESIZE_STAMP_SHIFT计算得到的,则rs2的二进制表示中,最高位一定为1,即rs2的值一直为负数。
前面我们介绍了sizeCtl的作用,若rs2就是sizeCtl,那么sizeCtl表示什么呢?rs2不等于-1,那么sizeCtl的取值就只有下面一种情况了:
sizeCtl = -(1 + n):当前有n个线程正在进行resize;
rs2转成十进制表示是
-2145714174
难道我们有2145714173个线程在做resize操作吗?肯定不是的。其实-(1 + n)中的(1 + n)只是sizeCtl的低16位了,rs2的低16位表示为十进制是2,即表示当前有1个线程在做resize操作,若有其他线程参与进来则,sizeCtl的值加1,。
通过上面的分析,我们知道,在扩容时sizeCtl的意义如下图所示:
高RESIZE_STAMP_BITS位 | 低RESIZE_STAMP_SHIFT位 |
---|---|
扩容标记 | 并行扩容线程数 |
看懂上面的分析之后,那tryPresize方法后面的程序大部分都可以看懂了,该方法最后会调用transfer()来进行真正的扩容处理
transfer方法
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// stride在单核下直接等于n,多核模式下为(n >>> 3) / NCPU
// stride可以理解为“步长”,表示每个线程处理桶的最小数目,可以看出核数越高步长越小,最小值是最小分割并行段数
//MIN_TRANSFER_STRIDE(16)
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//nextTab初始化,CAS保证了只会有一个线程执行这里的代码
// 如果新数组nextTab为null,先进行一次初始化,长度为旧table的2倍
if (nextTab == null) {
try {
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;
}
// 新table的长度
int nextn = nextTab.length;
// ForwardingNode就是正在被迁移的Node,ForwardingNode的hash值被设置成为了MOVED(-1),这个Node的key、value和next都为null
// 后面我们会看到,原数组中位置i处的节点完成迁移工作后
// 就会将位置i处的节点设置为这个ForwardingNode,用来告诉其他线程该位置已经处理过了
// 所以它其实相当于是一个标志
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 并发扩容的关键属性,如果advance等于true,说明这个节点已经处理过,可以处理下一个位置的节点了
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
// i是位置索引,bound是边界,注意是从后往前
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 这个while循环体的作用就是在控制i--
// 通过i--可以依次遍历原hash表中的节点
// 可以简单理解为:i指向了transferIndex,bound指向了transferIndex - stride
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, nextIndex)
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 如果所有的节点都已经完成复制工作,就把nextTable赋值给table
if (finishing) {
nextTable = null;
table = nextTab;
// 重新计算sizeCtl,n是原数组长度,所以计算得出的值将是新数组长度的0.75倍
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 之前我们说过,sizeCtl在迁移前会设置为(rs << RESIZE_STAMP_SHIFT) + 2
// 然后,每有一个线程参与迁移就会将sizeCtl加1
// 这里使用CAS操作对sizeCtl减1,代表该线程做完了属于自己的任务
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 任务结束,方法退出
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT
// 这表明所有的线程都完成了迁移工作,设置finishing为true,下次循环就会运行上面的if(finishing){}分支了
finishing = advance = true;
i = n; // recheck before commit
}
}
// 如果位置i处是空的,没有任何节点,那么放入刚刚初始化的ForwardingNode节点
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 该位置处是ForwardingNode节点,代表该位置已经迁移过了
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// 对数组该位置处的结点加锁,开始处理数组该位置处的迁移工作
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 头结点的hash值大于0,说明是链表节点
if (fh >= 0) {
// 下面这一段代码和JDK 1.7中的ConcurrentHashMap迁移差不多
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);
}
// 将其中的一个链表放在新数组的位置i
setTabAt(nextTab, i, ln);
// 将另一个链表放在新数组的位置i + n
setTabAt(nextTab, i + n, hn);
// 将原数组该位置处设置为fwd,代表该位置已经处理完毕
// 其他线程一旦看到该位置的hash值为 MOVED,就不会进行迁移了
setTabAt(tab, i, fwd);
// advance设置为true,代表该位置已经迁移完毕
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;
}
}
// 如果一分为二后,节点数少于UNTREEIFY_THRESHOLD,那么将红黑树转换回链表
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;
// 将ln放置在新数组的位置i
setTabAt(nextTab, i, ln);
// 将hn放置在新数组的位置i + n
setTabAt(nextTab, i + n, hn);
// 将原数组该位置处设置为fwd,代表该位置已经处理完毕
// 其他线程一旦看到该位置的hash值为 MOVED,就不会进行迁移了
setTabAt(tab, i, fwd);
// advance设置为true,代表该位置已经迁移完毕
advance = true;
}
}
}
}
}
}
我们这里介绍一下ForwardingNode的作用,它主要有两个:
- 标明此节点已完成迁移
- 为方便扩容期间的元素查找需求,里面有find()方法可以从nextTable查找元素
分片
每个线程针对一个分片来进行转移操作,所谓的一个分片其实就是bin数组的一段。默认的最小分片大小为16,如果所在机器 只有一个CPU核心,那么就取16,否则取(数组大小 / 8 / CPU核心数)与16的较大者。
transferIndex
全局变量transferIndex表示低于此值的bin尚未被转移,分片的申请便是通过对此变量的CAS操作来完成,初始值为原数组大小,减为0表示 所有桶位均已转移完毕。
ForwardingNode
从transfer方法的源码可以看出,当一个桶位(原数组)处理完时,会将其头结点设置一个ForwardingNode。简略版源码:
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
}
其哈希值为MOVED。到这里我们便可以理解putVal方法这部分源码的作用了:
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
helpTransfer方法的实现和tryPresize方法的相关代码很像
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 若tab不为null且首节点f是ForwardingNode节点,且f的nextTable不为null,即已经有其他线程在进行resize操作
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) {
// 扩容结束
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
// 帮助数据迁移,因为多了一个迁移线程,所以要将sizeCtl加1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
扩容操作总结
- 单线程新建nextTable,扩容为原table容量的两倍。
- 每个线程想增/删元素时,如果访问的桶是ForwardingNode节点,则表明当前正处于扩容状态,协助一起扩容完成后再完成相应的数据更改操作。
- 扩容时将原table的所有桶倒序分配,每个线程每次最小分16个桶进行处理,防止资源竞争导致的效率下降, 每个桶的迁移是单线程的,但桶范围处理分配可以多线程,在没有迁移完成所有桶之前每个线程需要重复获取迁移桶范围,直至所有桶迁移完成。
- 一个旧桶内的数据迁移完成但迁移工作没有全部完成时,查询数据委托给ForwardingNode结点查询nextTable完成(这个后面看find()分析)。
- 迁移过程中sizeCtl用于记录参与扩容线程的数量,全部迁移完成后sizeCtl更新为新table的扩容阈值。
将元素添加到table中之后,put方法最后还要更新元素的个数,我们来看一下addCount方法。
看完这个方法,其实还是不是很懂baseCount和counterCells的含义。。
看注释中写道,baseCount是在没有竞争时使用的变量,所以,我感觉在计算元素数目时,如果没有产生竞争,则用baseCount来记录,否则用counterCells记录了
计数
在putVal方法的结尾通过调用addCount方法(略去大小检查,扩容部分,这里我们只关心计数)进行计数:
// 增加节点个数,如果table太小而没有resize,则检查是否需要resize。如果已经调整大小,则可以帮助复制转移节点
// 转移后重新检查占用情况,以确定是否还需要调整大小,因为resize总是比put操作滞后
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 通过CAS操作更新baseCount
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
// 若counterCells不为null或者更新baseCount失败
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方法进行初始化
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
// check就是binCount,有新元素加入成功才检查是否要扩容
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// 元素数目大于当前扩容阈值并且小于最大扩容值才扩容,如果table还未初始化则等待初始化完成
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 返回table的扩容标记位
int rs = resizeStamp(n);
// 如果已经有其他线程在进行扩容
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 该线程参与扩容,则将sizeCtl的值加1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 没有线程在进行扩容,则该线程开始扩容,设置sizeCtl的值
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
计数的关键便是counterCells属性:
private transient volatile CounterCell[] counterCells;
CounterCell是ConcurrentHashMapd的内部类:
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
Contended注解的作用是将类的字段以64字节的填充行包围以解决伪共享问题。其实这里的计数方式就是改编自LongAdder,以最大程度地降低CAS失败空转的几率。
条件判断:
if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
//...
}
非常有意思 ,如果counterCells为null,那么尝试用baseCount进行计数,如果事实上只有一个线程或多个线程单竞争的频率较低,对baseCount的CAS操作并不会失败,所以可以得到结论 : 如果竞争程度较低(没有CAS失败),那么其实用的是volatile变量baseCount来计数,只有当线程竞争严重(出现CAS失败)时才会改用LongAdder的方式。
baseCount声明如下:
private transient volatile long baseCount;
再来看一下什么条件下会触发fullAddCount方法:
if (as == null || (m = as.length - 1) < 0 || (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
//...
}
ThreadLocalRandom.getProbe()的返回值决定了线程和哪一个CounterCell相关联,查看源码可以发现,此方法返回的其实是Thread的下列字段的值:
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
我们暂且不管这个值是怎么算出来,将其当做一个线程唯一的值即可。所以fullAddCount执行的条件是(或):
- CounterCell数组为null。
- CounterCell数组大小为0.
- CounterCell数组线程对应的下标值为null。
- CAS更新线程特定的CounterCell失败。
fullAddCount方法的实现其实和LongAdder的父类Striped64的longAccumulate大体一致:
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
//1.
if ((as = counterCells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
CounterCell r = new CounterCell(x); // Optimistic create
if (cellsBusy == 0 && U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
//新Cell创建成功,退出方法
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 && U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
//扩容
if (counterCells == as) {// Expand table unless stale
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
//rehash
h = ThreadLocalRandom.advanceProbe(h);
}
//2.
else if (cellsBusy == 0 && counterCells == as && U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try {
//获得锁之后再次检测是否已被初始化
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
//锁释放
cellsBusy = 0;
}
if (init)
//计数成功,退出方法
break;
}
//3.
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
从源码中可以看出,在初始情况下probe其实是0的,也就是说在一开始的时候都是更新到第一个cell中的,直到出现CAS失败。
整个方法的逻辑较为复杂,我们按照上面列出的fullAddCount执行条件进行对应说明。
cell数组为null或empty
容易看出,这里对应的是fullAddCount方法的源码2处。cellBusy的定义如下:
private transient volatile int cellsBusy;
这里其实将其当做锁来使用,即只允许在某一时刻只有一个线程正在进行CounterCell数组的初始化或扩容,其值为1说明有线程正在进行上述操作。
默认创建了大小为2的CounterCell数组。
下标为null或CAS失败
这里便对应源码的1处,各种条件分支不再展开详细描述,注意一下几点:
rehash
当Cell数组不为null和empty时,每次循环便会导致重新哈希值,这样做的目的是用再次生成哈希值的方式降低线程竞争。
最大CounterCell数
取NCPU:
static final int NCPU = Runtime.getRuntime().availableProcessors();
不过从上面扩容部分源码可以看出,最大值并不一定是NCPU,因为采用的是2倍扩容,准确来说是最小的大于等于NCPU的2的整次幂(初始大小为2)。
注意下面这个分支:
else if (counterCells != as || n >= NCPU)
collide = false;
此分支会将collide置为false,从而致使下次循环else if (!collide)
必定得到满足,这也就保证了扩容分支不会被执行。
baseCount分支
还会尝试对此变量进行更新,有意思。
size
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);
}
核心在于sumCount方法:
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
求和的时候带上了baseCount,剩下的就 一目了然了。
get
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算key对应节点的hash值
int h = spread(key.hashCode());
// 如果table不为null,table的长度不为0且对应下标存在节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 判断头结点是否就是我们查找的节点
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 如果头结点的hash值小于0,说明table正在扩容,或者e是红黑树节点
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 否则,遍历链表寻找匹配的节点
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
有意思的在于第二个分支,即哈希值小于零。从上面put方法部分可以得知,共有两种情况节点的哈希值小于0:
- ForwardingNode,已被转移。
- TreeBin,红黑树节点。
get方法整体还是比较简单的,如果头结点的hash值小于0,说明table正在扩容,或者e是红黑树节点,那我们来看一下,若table正在扩容时查找节点的代码,即ForwardingNode类实现的find方法:
Node<K,V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwarding nodes
outer: for (Node<K,V>[] tab = nextTable;;) {
Node<K,V> e; int n;
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (;;) {
int eh; K ek;
// 如果找到了匹配节点,则返回
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
// 若节点的hash值小于0
if (eh < 0) {
// 若节点是ForwardingNode节点
if (e instanceof ForwardingNode) {
// 将新创建的table赋值给tab,在新table中查找
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
// 否则,节点是红黑树节点
else
return e.find(h, k);
}
if ((e = e.next) == null)
return null;
}
}
}
再看一下红黑树实现的find方法,TreeBin.find:
final Node<K,V> find(int h, Object k) {
if (k != null) {
for (Node<K,V> e = first; e != null; ) {
int s; K ek;
if (((s = lockState) & (WAITER|WRITER)) != 0) {
if (e.hash == h && ((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
e = e.next;
}
else if (U.compareAndSwapInt(this, LOCKSTATE, s, s + READER)) {
TreeNode<K,V> r, p;
try {
p = ((r = root) == null ? null : r.findTreeNode(h, k, null));
} finally {
Thread w;
if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
(READER|WAITER) && (w = waiter) != null)
LockSupport.unpark(w);
}
return p;
}
}
}
return null;
}
这里使用了读写锁的方式,而加锁的方式和AQS一个套路。当可以获得读锁时,采用搜索红黑树的方法进行节点搜索,这样时间复杂度是O(LogN),而如果获得读锁失败(即表示当前有其它线程正在改变树的结构,比如进行红黑树的再平衡),那么将采用线性的搜索策略。
为什么可以进行线性搜索呢?因为红黑树的节点TreeNode继承自Node,所以仍然保留有next指针(即线性遍历的能力)。这一点可以从put-转为红黑树-红黑树一节得到反映,线性搜索的线程安全性通过next属性来保证:
volatile Node<K,V> next;
TreeBin的构造器同样对树的结构进行了改变,ConcurrentHashMap使用volatile读写来保证线程安全的发布。
从读写锁的引入可以看出,ConcurrentHashMap为保证最大程度的并行执行作出的努力。putTreeVal方法只有在更新树的结构时才会动用锁:
lockRoot();
try {
root = balanceInsertion(root, x);
} finally {
unlockRoot();
}
除此之外,由于读没有加锁,所以线程可以看到正在进行迁移的桶,但这其实并不会影响正确性,因为迁移是构造了新的链表,并不会影响原有的桶。
Remove
public V remove(Object key) {
return replaceNode(key, null, null);
}
final V replaceNode(Object key, V value, Object cv) {
// 计算key对应节点的hash值
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
// 若首节点对应hash值是MOVED,则表明该节点是ForwardingNode节点,帮助table扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
// 获取首节点的监视器锁
synchronized (f) {
if (tabAt(tab, i) == f) {
// 节点的hash值 >= 0,则表明节点是链表节点
if (fh >= 0) {
validated = true;
// 查找与key匹配的节点
for (Node<K,V> e = f, pred = null;;) {
K ek;
// 找到了匹配节点
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
// 如果cv为null或者cv与key对应的旧值ev“相等”
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
// 若value不等于null,则更新key节点对应的val
if (value != null)
e.val = value;
// 否则,将该节点删除
else if (pred != null)
pred.next = e.next;
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
// 节点是红黑树节点
else if (f instanceof TreeBin) {
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (validated) {
if (oldVal != null) {
// 若删除了一个节点,则更新元素数目
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
其他相关方法
要判断一个key在ConcurrentHashMap中是否存在,可以用get(Object)方法来判断的,因为ConcurrentHashMap中节点的key和value都不允许为null,而且,我们可以用containsKey(Object)方法来判断
public boolean containsKey(Object key) {
return get(key) != null;
}
clear()方法
public void clear() {
long delta = 0L; // negative number of deletions
int i = 0;
Node<K,V>[] tab = table;
// 遍历table数组
while (tab != null && i < tab.length) {
int fh;
// 获取table数组中下标为i的首节点
Node<K,V> f = tabAt(tab, i);
// 首节点为null,则更新下标i
if (f == null)
++i;
// 首节点hash值是MOVED,则帮助迁移
else if ((fh = f.hash) == MOVED) {
tab = helpTransfer(tab, f);
// 迁移完成之后,将i置为0,重新开始清除table
i = 0; // restart
}
else {
// 获取首节点的监视器锁
synchronized (f) {
// 根据Node节点的next属性,删除对应链表或者红黑树
if (tabAt(tab, i) == f) {
Node<K,V> p = (fh >= 0 ? f :
(f instanceof TreeBin) ?
((TreeBin<K,V>)f).first : null);
while (p != null) {
--delta;
p = p.next;
}
setTabAt(tab, i++, null);
}
}
}
}
// 更新元素数目
if (delta != 0L)
addCount(delta, -1);
}
相关问题
1、JDK 1.8为什么要放弃Segment?`
锁的粒度
首先锁的粒度并没有变粗,甚至变得更细了。每当扩容一次,ConcurrentHashMap的并发度就扩大一倍。
Hash冲突
JDK1.7中,ConcurrentHashMap从过二次hash的方式(Segment -> HashEntry)能够快速的找到查找的元素。在1.8中通过链表加红黑树的形式弥补了put、get时的性能差距。
扩容
JDK1.8中,在ConcurrentHashmap进行扩容时,其他线程可以通过检测数组中的节点决定是否对这条链表(红黑树)进行扩容,减小了扩容的粒度,提高了扩容的效率。
2、JDK 1.8为什么要使用synchronized而不是可重入锁?
减少内存开销
假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承AQS来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。
获得JVM的支持
可重入锁毕竟是API这个级别的,后续的性能优化空间很小。 synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。这就使得synchronized能够随着JDK版本的升级而不改动代码的前提下获得性能上的提升。
3、ConcurrentHashMap能完全替代HashTable吗?
hash table虽然性能上不如ConcurrentHashMap,但并不能完全被取代,两者的迭代器的一致性不同的,hash table的迭代器是强一致性的,而ConcurrentHashMap是弱一致的。 ConcurrentHashMap的迭代器方法都是弱一致性的。关于弱一致性的解释可以看这篇博客。
在JDK1.8的ConcurrentHashMap实现中,它的迭代器有KeySetView、ValuesView和EntrySetView这三种,我们来看获取KeySetView迭代器方的法:
/**
* <p>The view's iterators and spliterators are
* <a href="package-summary.html#Weakly"><i>weakly consistent</i></a>.
*
* @return the set view
*/
public KeySetView<K,V> keySet() {
KeySetView<K,V> ks;
return (ks = keySet) != null ? ks : (keySet = new KeySetView<K,V>(this, null));
}
可以看到,注释中也说明该迭代器是弱一致性的,我们来看一下KeySetView类的iterator方法:
public Iterator<K> iterator() {
Node<K,V>[] t;
ConcurrentHashMap<K,V> m = map;
int f = (t = m.table) == null ? 0 : t.length;
return new KeyIterator<K,V>(t, f, 0, f, m);
}
最终是返回了一个KeyIterator类对象,在KeyIterator上调用next方法时,最终实际调用到了Traverser.advance()方法,我们来看一下Traverser的构造方法以及advance()方法:
Traverser(Node<K,V>[] tab, int size, int index, int limit) {
this.tab = tab;
this.baseSize = size;
this.baseIndex = this.index = index;
this.baseLimit = limit;
this.next = null;
}
final Node<K,V> advance() {
Node<K,V> e;
if ((e = next) != null)
e = e.next;
for (;;) {
Node<K,V>[] t; int i, n; // must use locals in checks
if (e != null)
return next = e;
if (baseIndex >= baseLimit || (t = tab) == null ||
(n = t.length) <= (i = index) || i < 0)
return next = null;
if ((e = tabAt(t, i)) != null && e.hash < 0) {
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode<K,V>)e).nextTable;
e = null;
pushState(t, i, n);
continue;
}
else if (e instanceof TreeBin)
e = ((TreeBin<K,V>)e).first;
else
e = null;
}
if (stack != null)
recoverState(n);
else if ((index = i + baseSize) >= n)
index = ++baseIndex; // visit upper slots if present
}
}
这个方法在遍历底层数组。在遍历过程中,如果已经遍历的数组上的内容变化了,迭代器不会抛出ConcurrentModificationException异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭代过程中。这就是ConcurrentHashMap迭代器弱一致的表现。
但是Hashtable的任何操作都会把整个表锁住,是阻塞的。好处是总能获取最实时的更新,比如说线程A调用putAll写入大量数据,期间线程B调用迭代器方法,线程B就会被阻塞,直到线程A完成putAll,因此线程B肯定能获取到线程A写入的完整数据。坏处是所有调用都要排队,效率较低。
所以,ConcurrentHashMap并不能完全替代HashTable。