ConcurrentHashMap
JDK 8 ConcurrentHashMap源码分析
ForwardingNode在一个桶数组中的元素全部被搬迁完毕的时候会用来标记那个桶数组,表示它已经搬迁完毕了;同时还有一个作用就是当访问桶数组的时候,如果一个桶数组项已经被ForwardingNode标记了,那么就应该在新的桶数组(已经被扩容了的数组)中进行数据的访问;先扩容再红黑;大于8小于6;
构造器方法
// 初始容量,负载因子,并发度
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
// JDK8的加载方式是懒惰加载的,在构造方法中仅仅是计算了table的大小,在第一次使用的时候才会创建出table本身
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
// tableSizeFor保证最终计算出来的大小是2^n,即16, 32, 64等
// 因为后续的一些hash算法要求哈希表的大小都必须是2^n才能够正常工作
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
get方法
// get方法没有加锁,所以效率是很高的,并发度也很高
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// spread保证计算出的哈希码是正整数
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
// (n - 1) & h就相当于是一个取模运算,效率更高,找到桶数组的下标
// tabAt找到桶数组下标处的头结点
(e = tabAt(tab, (n - 1) & h)) != null) {
// 比较头结点的hash码是不是就是给出的key的hash码
if ((eh = e.hash) == h) {
// hash码是相同的,判断要查找的数和桶数组中头结点的key是不是一样的,如果是的话就直接返回值
// 如果用==判断出二者不是同一个对象的话就判断二者的值是不是相等的
// 也就是说不管二者是同一个对象还是二者的值是相等的都认为是同一个key
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// hash值为负数就表示该bin在扩容中是treebin(-2)
// 这时调用相应的find方法去红黑树中查找目标数据
// 或者头结点是ForwardingNode,ForwardingNode的hash值也是负数(-1)
// 此时去新的桶数组中查找数据
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方法
public V put(K key, V value) {
return putVal(key, value, false);
}
// 如果onlyIfAbsent取值为真,就表示只有第一次put键和值的时候才会将它们放入到map
// 之后遇到相同的key的时候并不会用新值覆盖掉旧的值,而是什么都不做
// 是false就表示会用新值覆盖掉旧值
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 普通的HashMap允许存在空的键和值
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)
// 创建hash表,使用的是CAS操作,只有一个线程会创建成功
// 如果在创建成功之后put操作还没有执行完毕,在下一轮循环会继续执行put
tab = initTable();
// 判断是否有头节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 通过CAS的方式创建头结点,如果创建失败了会再下一次循环中继续尝试将节点放置于map中
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 检查头节点是不是ForwardingNode
else if ((fh = f.hash) == MOVED)
// 锁住当前的链表帮忙扩容
tab = helpTransfer(tab, f);
// 既不是正在扩容,也不是正在初始化table,而是发生桶下标了冲突
else {
V oldVal = null;
// 只有在发生桶下标冲突的时候才加锁,并且加锁的粒度仅仅是链表的头节点
synchronized (f) {
if (tabAt(tab, i) == f) {
// fh >= 0的一定是普通节点,而不是红黑树的根节点或ForwardingNode
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;
// 赋值到旧的value值
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 已经是最后一个节点了,表示key不存在,新增Node追加到链表的结尾
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;
}
}
}
}
// 释放锁之后对链表进行优化,binCount != 0表示链表中存在冲突
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
// 如果binCount链表长度大小已经超过了树化的阈值就将链表转化为红黑树
// 注意不是立即树化的,而是先将链表进行扩容,如果还是存在binCount大于阈值的情况再进行树化
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 设置多个累加单元进行计数
addCount(1L, binCount);
return null;
}
initTable()方法
// 保证只有一个线程在创建table,其它的线程都是在忙等,并没有阻塞
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 判断table是不是已经创建了
while ((tab = table) == null || tab.length == 0) {
// 有其它的线程正在创建table
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
// 尝试将SZIECTL设置为-1,表示正在初始化table
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// sc表示初始容量
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// 此时的sc表示下次要扩容的时候的阈值
sc = n - (n >>> 2);
}
} finally {
// sizeCtl恢复为正数,其它正在忙等的线程会再次执行CAS操作
// 但是他们所执行的这次CAS操作还是会失败,因为sc的值已经不一样了
// 等待的线程再一次进入while,发现table已经被创建了,于是就退出循环
sizeCtl = sc;
}
break;
}
}
return tab;
}
addCount方法
// 该方法主要是维护哈希表的计数并执行扩容的操作
private final void addCount(long x, int check) {
// CounterCell[]是累加单元数组
CounterCell[] as; long b, s;
// 累加单元数组不为空的话说明就已经有竞争了,在累加单元之中进行累加
if ((as = counterCells) != null ||
// 如果累加单元数组还不存在的话就累加baseCount基础值
!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 ||
// 在累加单元中执行的CAS操作失败了
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 创建累加单元数组和累加单元cell,累加重试
fullAddCount(x, uncontended);
return;
}
// 检查链表的长度,如果链表的长度是小于等于1的,就直接返回
// 否则的话说明可能会需要扩容
if (check <= 1)
return;
// 获取元素的个数
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// sizeCtl是扩容的阈值
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
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;
// newtable已经被创建了,帮忙扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 需要扩容,将sc设置为一个负数,表示正在执行扩容的操作
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
// 调用transfer的时候新的哈希表是懒惰初始化的,tab是原来的哈希表
transfer(tab, null);
s = sumCount();
}
}
}
size()方法
// size计算实际发生在put,remove改变集合元素的操作之中
// 没有竞争的时候使用baseCount累加计数
// 存在竞争的时候新建counterCells,向其中一个cell累加计数
// counterCell起初的时候只有两个cell
// 如果竞争比较积累的话会创建新的cell来累加计数
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
// 将baseCount计数与所有cell计数累加
// 最终的计数不是特别的准确,因为是多线程的,有的在添加元素,有的在删除元素
// 很难的到最终的最新值,得到的是一个大概值
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
transfer方法
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 新table的初始化是懒惰的,所以在刚开始的时候一定是null
// if块中执行对新table的创建
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
// 将原有的容量左移1位,也就是乘2
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
}
}
// 如果链表头是null的话,就代表是已经处理完了
else if ((f = tabAt(tab, i)) == null)
// 将链表头设置为fwd,也就是ForwardingNode
advance = casTabAt(tab, i, null, fwd);
// 如果已经是ForwardingNode了,就会将advance置为true处理下一个链表
// static final int MOVED = -1; // hash for forwarding nodes
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
// 链表头是有元素的,就将链表锁住进行转移处理
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// fh >= 0表示的是普通节点
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;
}
}
}
}
}
}
JDK 7 ConcurrentHashMap源码分析
JDK 7 之中维护了一个segment数组,每一个segment对应着一把锁,优点就是每一个线程访问的是不同的segment数组,实际上是没有冲突的;但是segment数组的大小默认就是16,这个容量在初始化之后就已经被确定了,并且在之后是不能够改变的,segment数组的创建不是懒惰加载的;以下展示了JDK 7 ConcurrentHashMap的构造器方法:
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;
// ssize 必须是 2^n, 即 2, 4, 8, 16 ... 表示了 segments 数组的大小
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// 利用移位和掩码运算确定key对应的是segment数组中的哪一个元素
// segmentShift 默认是 32 - 4 = 28
this.segmentShift = 32 - sshift;
// segmentMask 默认是 15 即 0000 0000 0000 1111
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// 直接创建 segments and segments[0],不是懒惰加载的
// segments[0]是HashEntry,代表的是真正的哈希表
// 每个segment元素对应着一个哈希表,也就是HashEntry
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];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
put方法
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
// 计算出 segment 下标
// 对哈希码进行移位和掩码运算
int j = (hash >>> segmentShift) & segmentMask;
// 获得 segment 对象, 判断是否为 null, 是则创建该 segment
// segment数组和第一个元素虽然不是懒惰加载的,但是segment数组中的其它元素仍然是懒惰加载的
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null) {
// 这时不能确定是否真的为 null, 因为其它线程也发现该 segment 为 null,可能已经被创建出来了
// 因此在 ensureSegment 里用 cas 方式保证该 segment 安全性
s = ensureSegment(j);
}
// 进入 segment 的put 流程
return s.put(key, hash, value, false);
}
segment的put方法流程
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 尝试加锁
HashEntry<K,V> node = tryLock() ? null :
// 如果不成功, 进入 scanAndLockForPut 流程
// 如果是多核 CPU 最多 tryLock 64 次, 进入 lock 流程
// 在尝试期间, 还可以顺便看该节点在链表中有没有, 如果没有顺便创建出来
scanAndLockForPut(key, hash, value);
// 执行到这里 segment 已经被成功加锁, 可以安全执行
V oldValue;
try {
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和链表中的key是不是同一个kye
if ((k = e.key) == key ||
// 或者二者的hash码是相同的并且key的值是相同的(虽说通过 == 判断出不是同一个对象)
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
// 用新的值覆盖掉旧的值
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
// 如果在之前的流程中没有找到相同的key,就新增节点
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)
rehash(node);
else
// 将 node 作为链表头
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
该方法发生在put流程之中,put已经获取到了锁,所以该方法不需要考虑线程安全的问题
// 进行扩容的操作
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;
// 如果该链表中只有一个节点的话,就直接将节点移动到新的链表中
if (next == null) // Single node on list
newTable[idx] = e;
// 不然的话就尽可能的进行搬迁的工作而不是创建出新的节点
else { // Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
// 过一遍链表, 尽可能把 rehash 后 idx 不变的节点重用
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
// 从头结点开始遍历链表
// 查看原先的链表中哪一些节点的桶数组索引是没有改变的
// 将桶数组的索引值没有改变的节点直接迁移到新的桶数组之中
// 将剩余的没有迁移到新的桶数组中的节点在桶数组中创建出来
// 原先的桶数组会被垃圾收集器回收掉
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
newTable[lastIdx] = 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);
}
}
}
}
// 扩容完成, 才加入新的节点
int nodeIndex = node.hash & sizeMask; // add the new node
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 对象在数组中的偏移量,以定位到segment
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
// s 即为 segment
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;
}
size方法
// 计算个数的函数最终的结果可能是不准确的,因为是多线程运行,有的线程在增加节点,有的则在删除节点(弱一致性)
// 所以size方法就会采用一些方法来返回它认为是正确的结果
// 比如JDK7的实现就是先不加锁计算两次,如果两次得到的结果只是一样的,则认为计算出的个数就是正确的
// 如果不一样的话就进行重试,重试的次数超过3的时候就将所有的segment锁住,重新计算个数并返回
public int size() {
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 (;;) {
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) {
// 累计修改的计数
sum += seg.modCount;
// 累计单个seg中的元素的数量
int c = seg.count;
// 超过了int型数据所能够表示的最大值
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
// 修改计数的值是准确的,说明没有其他的线程进行数据的修改
// 如果不一致的话会重新进行尝试
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;
}
LinkedBlockingQueue 原理
基本的入队和出队操作:
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
static class Node<E> {
E item;
/**
* 下列三种情况之一
* - 真正的后继节点
* - 自己, 发生在出队时
* - null, 表示是没有后继节点, 是最后了
*/
Node<E> next;
Node(E x) { item = x; }
}
}
初始化链表 last = head = new Node<E>(null); Dummy节点是用来占位的,item为空:

当一个节点入队的时候: last = last.next = node;

再加入一个节点

出队的流程:
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;




加锁的流程:加锁设计的巧妙之处就是使用了两把锁和一个dummy节点:
- 用同一把锁的话在同一个时刻只会有一个线程在执行(生产者和消费者之中选一个);但是使用两把锁的话会有两个线程在同时执行(一个生产者与一个消费者);消费者与消费者之间仍然是串行执行的;同样生产者之间也是串行执行的;
- 并且以上的做法是线程安全的:因为有两个不同的加锁方法,分别是putLock()方法和takeLock()方法,当节点数大于2的时候(包括dummy节点),putLock()方法会保证last节点的线程安全,takeLock()方法会保证head节点的线程安全,两把锁保证了出队和入队的时候没有竞争;当节点数等于2的时候(包括dummy节点),仍然是两把锁锁住不同的对象,不会出现竞争;当节点数是1的时候(只有一个dummy节点),take线程会被notEmpty条件阻塞,即使存在竞争但是线程被阻塞了,仍然是安全的;
// 用于 put(阻塞) offer(非阻塞) private final ReentrantLock putLock = new ReentrantLock(); // 用户 take(阻塞) poll(非阻塞) private final ReentrantLock takeLock = new ReentrantLock();
put方法:
public void put(E e) throws InterruptedException {
// 不允许空的元素
if (e == null) throw new NullPointerException();
// 用来检查空位
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
// count 用来维护元素计数
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
// 满了就等待
while (count.get() == capacity) {
// 倒过来读就好: 等待 notFull
notFull.await();
}
// 有空位, 入队且计数加一
enqueue(node);
// 返回的是加一前的值
c = count.getAndIncrement();
// 传统的put操作在等待的时候是由消费者唤醒的,也就是消费者在take()之后会执行唤醒操作
// 但是在这里使用的是signal()来唤醒的
// 同时,不用signalAll()方法的原因是signalAll()会引起更多的竞争
// 增加了不必要的竞争
// 除了自己 put 以外, 队列还有空位, 由自己叫醒其他 put 线程
// c原本的值表示的就是执行put()之前的计数值,如果+1之后还是小于capacity的话
// 就表示队列中还是有空位的,由自己唤醒其它执行put()方法的线程;
if (c + 1 < capacity)
// 不是由消费者唤醒,而是自己唤醒自己(唤醒其它执行put操作但是正在等待的线程)
notFull.signal();
} finally {
putLock.unlock();
}
// 如果队列中只有一个元素, 叫醒执行 take() 方法的线程
if (c == 0)
// 这里调用的是 notEmpty.signal() 而不是 notEmpty.signalAll() 是为了减少竞争
signalNotEmpty();
}
take()方法:
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
// c > 1的话就表示队列中还存在资源,于是就唤醒其它在执行take()方法的线程
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
// 如果队列中只有一个空位时, 叫醒 put 线程
// 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c < capacity
// 保证了唤醒的操作不会被重复地执行
if (c == capacity)
// 这里调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争
signalNotFull()
return x;
}
LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较:
- 前者支持有界,后者强制有界;
- 前者基于链表,后者基于数组;
- 前者的初始化是懒惰的,后者需要提前初始化Node数组;
- 前者在每次入队的时候都会创建新的Node,后者的Node是提前创建好的;
- 前者使用两把锁,后者使用一把锁,因此前者的效率更高,也提倡使用后者;
初次之外ConcurrentLinkedQueue使用的也是两把锁以及dummy节点,但是ConcurrentLinkedQueue“锁”的实现是基于CAS的,所以ConcurrentLinkedQueue适用于并发程度更高的场景,因为CAS操作的并发度是要高于synchronized以及ReentrantLock的,比如在Tomcat中的SocketChannel使用的就是ConcurrentLinkedQueue;

CopyOnWriteArrayList底层采用的是写入时拷贝的思想,在进行增删改操作的时候会将底层的数组拷贝一份,更改的操作在新数组上执行,这时不影响其他线程的并发读,读读和读写都是并发的,只有写操作之间是互斥的,做到了读写分离;以新增为例:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取旧的数组
Object[] elements = getArray();
int len = elements.length;
// 拷贝新的数组(虽说这个过程是比较耗时的,但是不影响读操作的执行)
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 添加新元素
newElements[len] = e;
// 覆盖旧的数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
适合读多写少的场景;但是存在弱一致性:


数据库的MVCC也存在弱一致性;高并发和一致性是相互矛盾的,需要在二者之间寻找一个平衡点;
本文深入分析了Java中的线程安全集合类,重点关注ConcurrentHashMap在JDK 8和JDK 7的实现区别,以及LinkedBlockingQueue的工作原理。在JDK 8中,ConcurrentHashMap利用ForwardingNode实现扩容,而JDK 7通过Segment数组和锁实现并发。LinkedBlockingQueue的入队、出队操作通过两把锁确保线程安全,与ArrayBlockingQueue相比,其性能更优。此外,还提及CopyOnWriteArrayList的写入时拷贝策略,适合读多写少的场景。
1162





