线程安全集合类
早期线程安全集合类
1、如:Hashtable、Vector
2、所有方法都使用 synchronized,确保线程安全
3、性能差,不建议使用
Collections
1、使用 Collections 装饰的线程安全集合
(1)Collections.synchronizedCollection
(2)Collections.synchronizedList
(3)Collections.synchronizedMap
(4)Collections.synchronizedSet
(5)Collections.synchronizedNavigableMap
(6)Collections.synchronizedNavigableSet
(7)Collections.synchronizedSortedMap
(8)Collections.synchronizedSortedSet
2、装饰器模式
(1)使用内部类,保存传入的不安全集合
(2)只使用 synchronized 重写方法
(3)性能没有提高
java.util.concurrent.* 下的线程安全集合类
1、此包下的类的单方法线程安全,但组合方法线程不安全
2、Blocking
(1)大部分实现基于锁
(2)提供阻塞方法
3、CopyOnWrite
(1)拷贝再修改,避免并发安全
(2)修改开销相对较重
(3)适用于读多写少
4、Concurrent
(1)内部很多操作使用 CAS 优化,一般可以提供较高吞吐量
(2)遍历时弱一致性,如:当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,但内容是旧
(3)求大小弱一致性:size 操作未必是 100% 准确
(4)读取弱一致性
5、如果遍历时发生修改
(1)对于 java.util.concurrent.* 安全容器:使用 fail-safe 机制,如果容器发生修改,可以继续进行遍历,不抛出异常
(2)对于非安全容器:使用 fail-fast 机制,如果容器发生修改,则遍历立刻失败,抛出 ConcurrentModificationException,不再继续遍历
6、对于 CopyOnWrite、Concurrent 都有弱一致性
(1)如:数据库 MVCC(多版本并发控制)
(2)高并发、强一致性之间矛盾,需要权衡
HashMap
1、JDK 7
(1)头插法
(2)扩容时,存在并发死链问题
2、JDK 8
(1)尾插法
(2)扩容时,保持与扩容前相同顺序
JDK 8 ConcurrentHashMap
1、重要属性、内部类
// 默认为 0
// 当初始化时, 为 -1
// 当扩容时, 为 -(1 + 扩容线程数)
// 当初始化或扩容完成后,为 下一次的扩容的阈值大小
private transient volatile int sizeCtl;
// 整个 ConcurrentHashMap 就是一个 Node[]
static class Node<K,V> implements Map.Entry<K,V> {}
// hash 表
transient volatile Node<K,V>[] table;
// 扩容时,新 hash 表
private transient volatile Node<K,V>[] nextTable;
// 扩容时从后往前遍历Node[],迁移处理Node后,添加一个ForwardingNode作为头节点
// 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点
// 表示该下标的Node已迁移,会在新Node[]处理请求
static final class ForwardingNode<K,V> extends Node<K,V> {}
// 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node
static final class ReservationNode<K,V> extends Node<K,V> {}
// 作为 treebin 的头节点, 存储 root 和 first
static final class TreeBin<K,V> extends Node<K,V> {}
// 作为 treebin 的节点, 存储 parent, left, right
static final class TreeNode<K,V> extends Node<K,V> {}
2、重要方法
// 获取 Node[] 中第 i 个 Node
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i)
// cas 修改 Node[] 中第 i 个 Node 的值, c 为旧值, v 为新值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v)
// 直接修改 Node[] 中第 i 个 Node 的值, v 为新值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)
3、构造器
(1)懒惰初始化:只计算 table 大小,在第一次使用时才会真正创建
(2)设置的初始大小,不一定为所设定大小,必定为 2n
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
//计算初始大小
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
// tableSizeFor 保证计算的初始大小为 2^n
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
//确认下一次扩容容量,即创建时
this.sizeCtl = cap;
}
4、get
(1)无锁
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 &&
//tabAt定位桶下标,即定位数组下标的头节点,且不为null
//(n - 1) & h,相当于取模,但效率更高
(e = tabAt(tab, (n - 1) & h)) != null) {
//对比hash值
if ((eh = e.hash) == h) {
//对比是否相等
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// hash 为负数表示该 bin 在扩容中(ForwardingNode的hash小于0),或是 treebin(红黑树), 这时调用 find 方法来查找
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 正常遍历链表, 用 equals 比较
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
5、put
(1)以下数组简称(table),链表简称(bin)
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
//与HashMap不同,key、value都不允许为null
if (key == null || value == null) throw new NullPointerException();
//获取key的hash,其中spread会综合高位低位, 具有更好hash性
int hash = spread(key.hashCode());
//链表最大下标(长度)
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
// f 是链表头节点
// fh 是链表头节点的 hash
// i 是链表在 table 中的下标
Node<K,V> f; int n, i, fh;
//未创建Node[],则创建 table,懒惰初始化
if (tab == null || (n = tab.length) == 0)
// 初始化 table 使用CAS,不需要synchronized,创建成功,进入下一轮循环
tab = initTable();
//该下标未有头节点,则创建链表头节点
//tabAt(tab, i = (n - 1) & hash):查找桶下标
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 添加链表头使用CAS,无需 synchronized
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) {
//头节点hash大于0,则为链表
if (fh >= 0) {
binCount = 1;
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
//查找相同key
if (
//对比hash
e.hash == hash &&
//是否为同一对象
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
//取出旧value
oldVal = e.val;
//onlyIfAbsent:false:会用新value覆盖旧value;true:不会用新value覆盖旧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;
// putTreeVal 查看 key 是否已经在树中, 是, 则返回对应的 TreeNode
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
// 释放链表头节点的锁
}
//检查链表是否需要扩容或树化
//binCount != 0,说明链表有冲突
if (binCount != 0) {
// 如果链表长度 >= 树化阈值(8), 进行链表转为红黑树
if (binCount >= TREEIFY_THRESHOLD)
//尝试将链表转化为红黑树,需要链表长度 > 64,且仍有链表长度 >= 8
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
//类似LongAdder,设置多个累加单元
// 增加 size 计数
// addCount除了维护size计数,还有扩容逻辑
addCount(1L, binCount);
return null;
}
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//检查hash表是否创建,其它线程会在 while 循环中 yield 直至 table 创建
while ((tab = table) == null || tab.length == 0) {
//sc接收初始容量值,sc < 0,表示已有线程在创建hash表,其余线程进入忙等待
if ((sc = sizeCtl) < 0)
Thread.yield();
//CAS尝试将 SIZECTL 设置为 -1,表示初始化 table
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// 获得锁, 创建 table
try {
//再次检查hash表是否创建
if ((tab = table) == null || tab.length == 0) {
//初始容量 = 0,则使用默认值16,否则使用设定值
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
//计算下一次扩容时的阈值
sc = n - (n >>> 2);
}
} finally {
//下一次扩容时的阈值赋值到sizeCtl
sizeCtl = sc;
}
break;
}
}
//返回已创建的hash表
return tab;
}
//x:1L
//check:binCount,链表最大下标(长度)
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if (
// 存在CounterCell[],则在 counterCells 上累加
(as = counterCells) != null ||
// 不存在CounterCell[],向 baseCount 累加
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)
) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (
// 还没有CounterCell[]
as == null || (m = as.length - 1) < 0 ||
// CounterCell[]还没有CounterCell
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
// CAS增加CounterCell计数失败
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
) {
// 创建CounterCell[]和CounterCell,重试累加(死循环)
fullAddCount(x, uncontended);
return;
}
//链表长度 > 1,可能需要扩容
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) {
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);
}
// 需要扩容,newtable 未创建
//CAS将SIZECTL设为负数,表示进入扩容
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
//tab:原hash表
//null:新hash表,懒惰初始化
transfer(tab, null);
s = sumCount();
}
}
}
6、size
(1)size 计算实际发生在 put,remove,即改变集合元素的操作之中
(2)没有竞争发生,向 baseCount 累加计数
(3)有竞争发生,新建 CounterCell[],向其中的一个 CounterCell 累加计数
(4)CounterCell[] 初始有两个 CounterCell
(5)如果计数竞争比较激烈,会创建新的 CounterCell 来累加计数
(6)多线程情况下,获取所有节点数量值,可能存在偏差
public int size() {
//获取计数值
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
//并发时的个数变动保存在 CounterCell[] 当中
CounterCell[] as = counterCells; CounterCell a;
//节点个数保存在 baseCount 中
// 将 baseCount 计数与所有 CounterCell 计数累加
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
7、transfer
(1)以下数组简称(table),链表简称(bin)
(2)扩容时以 bin 为单位进行,需要对 bin 进行 synchronized
(3)其它竞争线程会帮助把其它 bin 进行扩容
(4)扩容时平均只有 1/6 的节点会复制到新 table 中
//tab:旧hash表
//nextTab:新hash表
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
//n:旧hash表(数组)长度
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//若新hash表为null,则初始化
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
//n << 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
}
}
//表示已移动该下标的链表头
else if ((f = tabAt(tab, i)) == null)
//将ForwardingNode替换链表头
advance = casTabAt(tab, i, null, fwd);
//表示该下标为已经为ForwardingNode,其hash == -1
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;
}
}
}
}
}
}
JDK 7 ConcurrentHashMap
1、维护一个 segment 数组,每个 segment 对应一把锁,Segment 继承 ReentrantLock
2、优点:如果多个线程访问不同 segment,实际没有冲突,类似 JDK 8
3、缺点
(1)Segments 数组默认大小为 16,容量初始化指定后就不能改变,固定并发度
(2)非懒惰初始化,空间占用不友好
4、构造器
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,表示 segments 数组的大小
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// segmentShift 默认是 32 - 4 = 28
this.segmentShift = 32 - sshift;
// segmentMask 默认是 15,即 00000000 00001111
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,同时创建 segments[0],即 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;
}
5、put
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
// 计算出 segment 下标
// segmentShift、segmentMask 将 key 的 hash 结果匹配到 segment
// 根据某一 hash 值求 segment 位置,先将hash 逻辑右移 this.segmentShift 位,结果再与 this.segmentMask 做位于运算
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 安全性
s = ensureSegment(j);
}
// 进入 segment 的put 流程
return s.put(key, hash, value, false);
}
(1)Segment 继承可重入锁(ReentrantLock)
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;
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)
rehash(node);
else
// 将 node 作为链表头
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
(2)扩容:rehash 发生在 put 中,因为此时已经获得了锁,因此 rehash 时不需要考虑线程安全
private void rehash(HashEntry<K,V> node) {
//旧hash表
HashEntry<K,V>[] oldTable = table;
//旧容量
int oldCapacity = oldTable.length;
//新容量 = 旧容量 * 2
int newCapacity = oldCapacity << 1;
//新扩容阈值
threshold = (int)(newCapacity * loadFactor);
//空的新hash表
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
//遍历旧hsah表
for (int i = 0; i < oldCapacity ; i++) {
//e:当前遍历数组的节点
HashEntry<K,V> e = oldTable[i];
if (e != null) {
//next:下一节点
HashEntry<K,V> next = e.next;
//e在新数组的下标
int idx = e.hash & sizeMask;
//链表只有一个节点,即e为头节点,有且只有一个
if (next == null) // Single node on list
//e直接迁移
newTable[idx] = e;
//链表不止一个节点
else { // Reuse consecutive sequence at same slot
//lastRun:当前链表节点
HashEntry<K,V> lastRun = e;
//lastIdx:在新数组的下标
int lastIdx = idx;
// 遍历链表, 尽可能重用 rehash 后 idx 不变的节点
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
//k:下一节点在新数组的下标
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;
}
6、get
(1)不加锁,使用 UNSAFE 方法保证可见性,JDK 8 中同样使用 Unsafe 方法,只是封装后不可见
(2)扩容过程中,get 先发生就从旧表取内容,get 后发生就从新表取内容
(3)getObjectVolatile:不论是 Segment 或 HashEntry,都属于数组元素,只用 volatile 修饰数组不能保证可见性,需要使用 getObjectVolatile 保证数组的可见性
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.length - 1) & h:定位桶下标
(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;
}
7、size
(1)计算元素个数前,先不加锁计算两次,如果前后两次结果一样,认为个数正确返回
(2)如果不一样,进行重试,重试次数超过 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 == 2,retries = -1,总共有 3 次机会
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;
//遍历Segment[]
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
//seg.modCount:最近修改(put、remove)计数
sum += seg.modCount;
//seg.count:当前Segment的元素个数
int c = seg.count;
if (c < 0 || (size += c) < 0)
//计数溢出int最大值
overflow = true;
}
}
//若相等,说明统计期间没有干扰
//若不等,重新尝试统计
if (sum == last)
break;
last = sum;
}
} finally {
//retries == 3,即第 3 次失败后
if (retries > RETRIES_BEFORE_LOCK) {
//释放segment所有锁
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
LinkedBlockingQueue
1、阻塞单向链表
2、节点
public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
static class Node<E> {
E item;
//next三种情况
//真正的后继节点
//节点自身,发生在出队时
//null, 表示没有后继节点
Node<E> next;
Node(E x) {
item = x;
}
}
}
3、初始化链表
(1)last = head = new Node(null);
(2)Dummy 节点用来占位,item 为 null
4、入队
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
5、出队
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node<E> h = head;
//防止第二个节点不被GC
Node<E> first = h.next;
//next指向自己,保证原头节点被GC
h.next = h; // help GC
//第二个节点成为新的头节点
head = first;
E x = first.item;
//第二个节点置空,即成为哨兵节点
first.item = null;
return x;
}
6、加锁分析
(1)一把锁,同一时刻,最多只允许有一个线程(生产者或消费者)执行
(2)两把锁,同一时刻,可以允许两个线程(一个生产者、一个消费者)同时执行
(3)消费者与消费者线程仍然串行
(4)生产者与生产者线程仍然串行
7、线程安全分析
// 用于 put(阻塞) offer(非阻塞)
private final ReentrantLock putLock = new ReentrantLock();
// 用户 take(阻塞) poll(非阻塞)
private final ReentrantLock takeLock = new ReentrantLock();
(1)当节点总数大于 2 时(包括 Dummy 节点),putLock 保证 last 节点的线程安全,takeLock 保证 head 节点的线程安全,两把锁保证入队、出队没有竞争
(2)当节点总数等于 2 时(即一个 Dummy 节点,一个正常节点),此时仍是两把锁,锁两个对象,不会竞争
(3)当节点总数等于 1 时(只有一个 Dummy 节点),这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞
8、put
public void put(E e) throws InterruptedException {
//不允许元素为null
if (e == null) throw new NullPointerException();
int c = -1;
//使用Node包装e
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 以外, 队列还有空位, 由自己叫醒其他 put 线程
if (c + 1 < capacity)
//使用signal,而不是signalAll,减少竞争
notFull.signal();
} finally {
putLock.unlock();
}
// 如果队列中有一个元素, 唤醒 take 线程
if (c == 0)
// 调用 notEmpty.signal(),而不是 notEmpty.signalAll(),为了减少竞争
signalNotEmpty();
}
9、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();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
// 如果队列中只有一个空位时, 叫醒 put 线程
// 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c < capacity
if (c == capacity)
// 这里调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争
signalNotFull()
return x;
}
10、LinkedBlockingQueue、ArrayBlockingQueue 性能比较
(1)LinkedBlockingQueue 支持有界;ArrayBlockingQueue 强制有界
(2)LinkedBlockingQueue 实现是链表;ArrayBlockingQueue 实现是数组
(3)LinkedBlockingQueue 是懒惰初始化;ArrayBlockingQueue 需要提前初始化 Node 数组,但数组可重用内存,减少 GC
(4)LinkedBlockingQueue 每次入队会生成新 Node;ArrayBlockingQueue 提前创建 Node
(5)LinkedBlockingQueue 两把锁;ArrayBlockingQueue 一把锁
ConcurrentLinkedQueue
1、与 LinkedBlockingQueue 类似
(1)两把锁,同一时刻,可以允许两个线程(一个生产者、一个消费者)同时执行
(2)Dummy 节点,让两把锁,将来锁住的是不同对象,避免竞争
(3)区别:使用 CAS 实现锁
2、应用:如:Tomcat 的 Connector 结构时,Acceptor 作为生产者向 Poller 消费者传递事件信息时,采用 ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用
CopyOnWriteArrayList
1、CopyOnWriteArraySet
(1)内部维护 CopyOnWriteArrayList
private final CopyOnWriteArrayList<E> al;
(2)都通过 al 调用方法
(3)区别:Set 的元素唯一
2、读写分离
(1)底层实现:写入时拷贝
(2)增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,不影响其它线程的并发读
3、应用场景:读多写少
4、相当于线程安全的 ArrayList,都为可变数组
5、与 ArrayList 不同的特性
(1)它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突
(2)线程安全
(3)因为通常需要复制整个基础数组,所以可变操作的开销很大
(4)迭代器支持 hasNext()、next() 等不可变操作,但不支持可变 remove() 等操作
(5)使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照
6、动态数组
(1)内部有 volatile 数组维护数据
(2)在添加 / 修改 / 删除数据时,都会新建一个数组,并将更新后的数据,拷贝到新建的数组中,最后再将该数组赋值给 volatile 数组
7、线程安全
(1)通过 volatile、互斥锁实现
(2)通过 volatile 数组保存数据,一个线程读取 volatile 数组时,总能看到其它线程对该 volatile 变量最后的写入
(3)通过互斥锁来保护数据,在添加 / 修改 / 删除数据时,先获取互斥锁,修改完毕后,先将数据更新到 volatile 数组中,然后再释放互斥锁
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战