ConcurrentHashMap 的使用和原理
Map 是一个接口,它的实现方式有很多种,比如常见的 HashMap、LinkedHashMap,但是这些 Map 的实现并不是线程安全的,在多线程高并发的环境中会出现线程安全的问题。
鉴于 Map 是一个在高并发的应用环境中应用比较广泛的数据结构,Doug Lea 自 JDK 1.5 版本起在 Java 中引入了 ConcurrentHashMap
ConcurrentHashMap 的使用
对一个技术的掌握,从使用开始。我们现在来实现一个一个高并发计数器,例如记录网站访问量、接口调用次数等。
arduino
@Service
public class Counter {
private final ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
public void increase(String key) {
map.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
}
public int get(String key) {
return map.getOrDefault(key, 0);
}
}
compute
:这是一个并发安全原子操作,我们使用compute
方法实现对计数器的增加操作。
a.如果 key 不存在则新建一个值为 1 的计数器。
b.否则将其 value 递增 1。
- 通过 get 方法可以获取指定 key 对应的计数器值。
这个例子比较简单,现在开始上强度。ConcurrentHashMap 还可以用来实现缓存管理器,例如存储经常使用的业务数据、系统配置等信息,从而避免频繁的数据库查询或网络请求。
以下是一个支持过期时间、自动刷新和并发控制的缓存管理器实现,包含详细注释和最佳实践:
scss
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
/**
* 高性能并发缓存管理器
* @param <K> 键类型
* @param <V> 值类型
*/
publicclass ConcurrentCache<K, V> {
privatefinal ConcurrentHashMap<K, CacheEntry<V>> cache = new ConcurrentHashMap<>();
privatefinal Function<K, V> loader; // 缓存加载器
privatefinal ScheduledExecutorService cleaner; // 过期清理线程
// 默认配置
privatelong defaultTTL = 30_000; // 默认30秒
privatelong cleanupInterval = 5_000; // 5秒清理一次
privateint maxRetries = 3; // 最大重试次数
privateboolean refreshOnAccess = true; // 访问时刷新TTL
public ConcurrentCache(Function<K, V> loader) {
this.loader = loader;
this.cleaner = Executors.newSingleThreadScheduledExecutor();
startCleanupTask();
}
/**
* 获取缓存值(线程安全)
*/
public V get(K key) {
CacheEntry<V> entry = cache.get(key);
// 无缓存或已过期时加载
if (entry == null || entry.isExpired()) {
return loadAndCache(key);
}
// 更新访问时间(可选)
if (refreshOnAccess) {
entry.touch();
}
return entry.value;
}
/**
* 原子性的加载和缓存操作
*/
private V loadAndCache(K key) {
int retry = 0;
while (retry++ < maxRetries) {
try {
// 使用compute保证原子性
CacheEntry<V> newEntry = cache.compute(key, (k, oldEntry) -> {
// 检查其他线程是否已经加载
if (oldEntry != null && !oldEntry.isExpired()) {
return oldEntry;
}
V value = loader.apply(k);
returnnew CacheEntry<>(value, defaultTTL, TimeUnit.MILLISECONDS);
});
return newEntry.value;
} catch (Exception ex) {
if (retry >= maxRetries) {
thrownew CacheLoadException("加载缓存失败,key=" + key, ex);
}
// 指数退避重试
sleepUninterruptibly((long) Math.pow(2, retry), TimeUnit.MILLISECONDS);
}
}
thrownew CacheLoadException("超过最大重试次数,key=" + key);
}
/**
* 主动放入缓存(支持自定义TTL)
*/
public void put(K key, V value, long ttl, TimeUnit unit) {
cache.put(key, new CacheEntry<>(value, ttl, unit));
}
/**
* 启动定期清理任务(双重检查锁模式)
*/
private void startCleanupTask() {
if (cleaner.isShutdown()) return;
cleaner.scheduleWithFixedDelay(() -> {
cache.forEach((key, entry) -> {
if (entry.isExpired()) {
cache.remove(key, entry); // 使用CAS删除
}
});
}, cleanupInterval, cleanupInterval, TimeUnit.MILLISECONDS);
}
// 其他实用方法
public void remove(K key) { cache.remove(key); }
public void clear() { cache.clear(); }
public long size() { return cache.mappingCount(); }
// 配置方法(Builder模式风格)
public ConcurrentCache<K, V> defaultTTL(long ttl, TimeUnit unit) {
this.defaultTTL = unit.toMillis(ttl);
returnthis;
}
public ConcurrentCache<K, V> cleanupInterval(long interval, TimeUnit unit) {
this.cleanupInterval = unit.toMillis(interval);
returnthis;
}
// 异常处理
privatestaticclass CacheLoadException extends RuntimeException {
CacheLoadException(String message, Throwable cause) {
super(message, cause);
}
CacheLoadException(String message) {
super(message);
}
}
// 工具方法:不可中断的休眠
private static void sleepUninterruptibly(long duration, TimeUnit unit) {
try {
Thread.sleep(unit.toMillis(duration));
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
// 关闭时释放资源
public void shutdown() {
cleaner.shutdownNow();
cache.clear();
}
// 缓存条目:包含值、过期时间和访问时间戳
privatestaticclass CacheEntry<V> {
final V value;
finallong expireAt; // 绝对过期时间(纳秒)
final AtomicLong accessTime = new AtomicLong(); // 最后访问时间(纳秒)
CacheEntry(V value, long ttl, TimeUnit unit) {
this.value = value;
this.expireAt = System.nanoTime() + unit.toNanos(ttl);
touch();
}
// 刷新访问时间
void touch() {
accessTime.set(System.nanoTime());
}
// 判断是否已过期
boolean isExpired() {
return System.nanoTime() > expireAt;
}
}
}
实现原理
CHM 的源码有 6k 多行,包含的内容多,精巧,不容易理解;建议在查看源码的时候,可以首先把握整体结构脉络,对于一些精巧的优化,哈希技巧可以先了解目的就可以了,不用深究。
对整体把握比较清楚后,在逐步分析,可以比较快速的看懂。
JDK1.8 版本中的 CHM,和 JDK1.7 版本的差别非常大,在查看资料的时候要注意区分,1.7 中主要是使用 Segment 分段锁 来解决并发问题的。
JDK 1.7 版本 ConcurrentHashMap
在 JDK1.7 版本中,ConcurrentHashMap 的数据结构是由一个 Segment 数组和多个 HashEntry 组成。
而每一个 Segment 元素存储的是 HashEntry 数组+链表,并对应一个 ReentrantLock 锁,用于并发访问控制。
以 put 操作为例,来看一下 ConcurrentHashMap 的实现过程:
- 首先计算 key 的哈希值。
- 根据哈希值找到对应的 Segment。
- 获取 Segment 对应的锁。
- 如果还没有元素,就直接插入到 Segment 中。
- 如果已经存在元素,就循环比较 key 是否相等。
- 如果 key 已经存在,就根据要求更新 value。
- 如果 key 不存在,就插入新的元素(链表或者红黑树)。
上述操作中,步骤 2 到 3 相当于对对应的 Segment 加了一个悲观锁,如果 Segment 数组只有一个 Segment 元素,效果与 Hashtable 类似。
如果存在多个 Segment,效果就相当于使用了分段锁机制,提高了并发访问性能。
JDK 1.8 ConcurrentHashMap
在 JDK1.8 中,ConcurrentHashMap 的实现原理摒弃了这种设计,而是选择了与 HashMap 类似的数组+链表+红黑树的方式实现,而加锁则采用 CAS 和 synchronized 实现。

其主要区别就在 CHM 支持并发:
- 使用 Unsafe 方法操作数组内部元素,保证可见性;(
U.getObjectVolatile、U.compareAndSwapObject、U.putObjectVolatile
)。 - 在更新和移动节点的时候,直接锁住对应的哈希桶,锁粒度更小,且动态扩展。
- 针对扩容慢操作进行优化。
a.首先扩容过程的中,节点首先移动到过度表 nextTable ,所有节点移动完毕时替换散列表 table。
b.移动时先将散列表定长等分,然后逆序依次领取任务扩容,设置 sizeCtl 标记正在扩容。
c.移动完成一个哈希桶或者遇到空桶时,将其标记为 ForwardingNode 节点,并指向 nextTable 。
d.后有其他线程在操作哈希表时,遇到 ForwardingNode 节点,则先帮助扩容(继续领取分段任务),扩容完成后再继续之前的操作。
- 优化哈希表计数器,采用 LongAdder、Striped64 类似思想。
- 以及大量的哈希算法优化和状态变量优化。
类定义和成员变量
ini
// node数组最大容量:2^30=1073741824
privatestaticfinalint MAXIMUM_CAPACITY = 1 << 30;
// 默认初始值,必须是2的幕数
privatestaticfinalint DEFAULT_CAPACITY = 16;
//数组可能最大值,需要与toArray()相关方法关联
staticfinalint MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//并发级别,遗留下来的,为兼容以前的版本
privatestaticfinalint DEFAULT_CONCURRENCY_LEVEL = 16;
// 负载因子
privatestaticfinalfloat LOAD_FACTOR = 0.75f;
// 链表转红黑树阀值,> 8 链表转换为红黑树
staticfinalint TREEIFY_THRESHOLD = 8;
//树转链表阀值,小于等于6(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,<=UNTREEIFY_THRESHOLD 则untreeify(lo))
staticfinalint UNTREEIFY_THRESHOLD = 6;
staticfinalint MIN_TREEIFY_CAPACITY = 64;
privatestaticfinalint MIN_TRANSFER_STRIDE = 16;
privatestaticint RESIZE_STAMP_BITS = 16;
// 2^15-1,help resize的最大线程数
privatestaticfinalint MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 32-16=16,sizeCtl中记录size大小的偏移量
privatestaticfinalint RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
staticfinalint MOVED = -1;
// 树根节点的hash值
staticfinalint TREEBIN = -2;
// ReservationNode的hash值
staticfinalint RESERVED = -3;
// 可用处理器数量
staticfinalint NCPU = Runtime.getRuntime().availableProcessors();
//存放node的数组
transientvolatile Node<K,V>[] table;
/*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
*当为负数时:-1代表正在初始化,-N代表有N-1个线程正在 进行扩容
*当为0时:代表当时的table还没有被初始化
*当为正数时:表示初始化或者下一次进行扩容的大小
*/
privatetransientvolatileint sizeCtl;
上面有几个重要的地方这里单独讲:
LOAD_FACTOR:
这里的负载系数,同 HashMap 等其他 Map 的系数有明显区别:
- 通常的系数默认 0.75,可以由构造函数传入,当节点数 size 超过 loadFactor * capacity 时扩容。
- 而 CMH 的系数则固定 0.75(使用
n - (n >>> 2)
表示),构造函数传入的系数只影响初始化容量,见第 5 个构造函数。
sizeCtl:
sizeCtl 是 CHM 中最重要的状态变量,其中包括很多中状态,这里先整体介绍帮助后面源码理解。
- sizeCtl = 0 :初始值,还未指定初始容量。
- sizeCtl > 0 :
a.table 未初始化,表示初始化容量。
b.table 已初始化,表示扩容阈值(0.75n)。
- sizeCtl = -1 :表示正在初始化。
- sizeCtl < -1 :表示正在扩容,具体结构如图所示:

Node 节点
Node 是 ConcurrentHashMap 存储结构的基本单元,继承于 HashMap 中的 Entry,用于存储数据,源代码如下。
typescript
static class Node<K,V> implements Map.Entry<K,V> { // 哈希表普通节点
finalint hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node<K,V> find(int h, Object k) {} // 主要在扩容时,利用多态查询已转移节点
}
staticfinalclass ForwardingNode<K,V> extends Node<K,V> { // 标识扩容节点
final Node<K,V>[] nextTable; // 指向成员变量 ConcurrentHashMap.nextTable
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null); // hash = -1,快速确定 ForwardingNode 节点
this.nextTable = tab;
}
Node<K,V> find(int h, Object k) {}
}
staticfinalclass TreeBin<K,V> extends Node<K,V> { // 红黑树根节点
TreeBin(TreeNode<K,V> b) {
super(TREEBIN, null, null, null); // hash = -2,快速确定红黑树,
...
}
}
staticfinalclass TreeNode<K,V> extends Node<K,V> { } // 红黑树普通节点,其 hash 同 Node 普通节点 > 0;
哈希计算
java
static finalint MOVED = -1; // hash for forwarding nodes
staticfinalint TREEBIN = -2; // hash for roots of trees
staticfinalint RESERVED = -3; // hash for transient reservations
staticfinalint HASH_BITS = 0x7fffffff; // usable bits of normal node hash
// 让高位16位,参与哈希桶定位运算的同时,保证 hash 为正
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
哈希桶可见性
一个数组即使声明为 volatile
,也只能保证这个数组引用本身的可见性,其内部元素的可见性是无法保证的,如果每次都加锁,则效率必然大大降低,在 CHM 中则使用 Unsafe
方法来保证:
arduino
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
staticfinal <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
staticfinal <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
put 操作
思路是对当前的 table 进行无条件自循环直到 put 成功,可以分成以下六步流程来概述。
- 如果没有初始化就先调用 initTable()方法来进行初始化过程。
- 如果没有 hash 冲突就直接 CAS 插入。
- 如果还在进行扩容操作就先进行扩容。
- 如果存在 hash 冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入。
- 最后一个如果该链表的数量大于阈值 8,就要先转换成黑红树的结构,break 再一次进入循环。
- 如果添加成功就调用 addCount()方法统计 size,并且检查是否需要扩容。
ini
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) thrownew NullPointerException();
int hash = spread(key.hashCode()); //两次hash,减少hash冲突,可以均匀分布
int binCount = 0;
for (Node<K,V>[] tab = table;;) { //对这个table进行迭代
Node<K,V> f; int n, i, fh;
//这里就是上面构造方法没有进行初始化,在这里进行判断,为null就调用initTable进行初始化,属于懒汉模式初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
elseif ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//如果i位置没有数据,就直接无锁插入
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
elseif ((fh = f.hash) == MOVED)//如果在进行扩容,则先进行扩容操作
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//如果以上条件都不满足,那就要进行加锁操作,也就是存在hash冲突,锁住链表或者红黑树的头结点
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) { //表示该节点是链表结构
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//这里涉及到相同的key进行put就会覆盖原先的value
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
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;
}
}
}
elseif (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;
}
}
}
}
if (binCount != 0) { //如果链表的长度大于8时就会进行红黑树的转换
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);//统计size,并且检查是否需要扩容
returnnull;
}
流程图如下所示:

get 操作
get 方法可能看代码不是很长,但是他却能 保证无锁状态下的内存一致性 。
kotlin
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode()); // 计算 hash
if ((tab = table) != null && (n = tab.length) > 0 && // 确保 table 已经初始化
// 确保对应的哈希桶不为空,注意这里是 Volatile 语义获取;因为扩容的时候,是完全拷贝,所以只要不为空,则链表必然完整
(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,则必然在扩容,原来位置的节点可能全部移动到 i + oldCap 位置,所以利用多态到 nextTable 中查找
elseif (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;
}
ConcurrentHashMap 的 get 操作的流程很简单,也很清晰,可以分为三个步骤来描述。
- 计算 hash 值,定位到该 table 索引位置,如果是首节点符合就返回。
- 如果遇到扩容的时候,会调用标志正在扩容节点 ForwardingNode 的 find 方法,查找该节点,匹配就返回。
- 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回 null。
size 操作
在 JDK1.8 版本中,对于 size 的计算,在扩容和 addCount()方法就已经有处理了,JDK1.7 是在调用 size()方法才去计算,其实在并发集合中去计算 size 是没有多大的意义的,因为 size 是实时在变的,只能计算某一刻的大小,但是某一刻太快了,人的感知是一个时间段,所以并不是很精确。
扩容
扩容操作一直都是比较慢的操作,而 CHM 中巧妙的利用任务划分,使得多个线程可能同时参与扩容。
另外扩容条件也有两个:
- 有链表长度超过 8,但是容量小于 64 的时候,发生扩容。
- 节点数超过阈值的时候,发生扩容。
其扩容的过程可描述为:
- 首先扩容过程的中,节点首先移动到过度表 nextTable ,所有节点移动完毕时替换散列表 table。
- 移动时先将散列表定长等分,然后逆序依次领取任务扩容,设置 sizeCtl 标记正在扩容。
- 移动完成一个哈希桶或者遇到空桶时,将其标记为 ForwardingNode 节点,并指向 nextTable 。
- 后有其他线程在操作哈希表时,遇到 ForwardingNode 节点,则先帮助扩容(继续领取分段任务),扩容完成后再继续之前的操作。
如图:

具体源码如下。
ini
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; // 根据 CPU 数量计算任务步长
if (nextTab == null) { // 初始化 nextTab
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 扩容一倍
nextTab = nt;
} catch (Throwable ex) {
sizeCtl = Integer.MAX_VALUE; // 发生 OOM 时,不再扩容
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;
elseif ((nextIndex = transferIndex) <= 0) { // 遍历到达头节点,已经没有待迁移的桶,线程准备退出
i = -1;
advance = false;
}
elseif (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { // 当前任务完成,领取下一批哈希桶
bound = nextBound;
i = nextIndex - 1; // 索引指向下一批哈希桶
advance = false;
}
}
// i < 0 :表示扩容结束,已经没有待移动的哈希桶
// i >= n :扩容结束,再次检查确认
// i + n >= nextn : 在使用 nextTable 替换 table 时,有线程进入扩容就会出现
if (i < 0 || i >= n || i + n >= nextn) { // 完成扩容准备退出
int sc;
if (finishing) { // 两次检查,只有最后一个扩容线程退出时,才更新变量
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1); // 0.75*2*n
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
}
}
elseif ((f = tabAt(tab, i)) == null) // 当前节点为空,直接标记为 ForwardingNode,然后继续获取下一个桶
advance = casTabAt(tab, i, null, fwd);
// 之前的线程已经完成该桶的移动,直接跳过,正常情况下自己的任务区间,不会出现 ForwardingNode 节点,
elseif ((fh = f.hash) == MOVED) // 此处为极端条件下的健壮性检查
advance = true; // already processed
// 开始处理链表
else {
// 注意在 get 的时候,可以无锁获取,是因为扩容是全拷贝节点,完成后最后在更新哈希桶
// 而在 put 的时候,是直接将节点加入尾部,获取修改其中的值,此时如果允许 put 操作,最后就会发生脏读,
// 所以 put 和 transfer,需要竞争同一把锁,也就是对应的哈希桶,以保证内存一致性效果
synchronized (f) {
if (tabAt(tab, i) == f) { // 确认锁定的是同一个桶
Node<K,V> ln, hn;
if (fh >= 0) { // 正常节点
int runBit = fh & n; // hash & 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;
}
// 依次将链表拆分成,lo、hi 两条链表,即位置不变的链表,和位置 + oldCap 的链表
// 注意最后一节链表没有new,而是直接使用原来的节点
// 同时链表的顺序也被打乱了,lastRun 到最后为正序,前面一节为逆序
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); // 插入 lo 链表
setTabAt(nextTab, i + n, hn); // 插入 hi 链表
setTabAt(tab, i, fwd); // 哈希桶移动完成,标记为 ForwardingNode 节点
advance = true; // 继续获取下一个桶
}
elseif (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; // 同样记录 lo,hi 链表的长度
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;
}
}
}
}
}
}
ConcurrentLinkedQueue 的使用和原理
ConcurerntLinkedQueue 一个基于单向链表的无界线程安全队列,支持高并发的队列操作,无需显式的锁,而且容量没有上限。
此队列按照 FIFO(先进先出)原则对元素进行排序。新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。

应用场景
常见的使用场景可能包括任务调度、事件处理、日志记录等。比如订单处理系统,其中多个生产者生成订单,多个消费者处理订单。
csharp
// 订单事件处理器(生产环境级实现)
publicclass OrderEventProcessor {
// 使用队列作为订单缓冲区(无容量限制)
privatefinal ConcurrentLinkedQueue<OrderEvent> queue = new ConcurrentLinkedQueue<>();
privatefinal ExecutorService workers = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * 2,
new NamedThreadFactory("order-processor")
);
// 初始化处理线程
public void start() {
for (int i = 0; i < workers.getCorePoolSize(); i++) {
workers.submit(this::processEvents);
}
}
// 接收订单事件(来自网络IO线程)
public void receiveEvent(OrderEvent event) {
queue.offer(event); // 无阻塞插入
metrics.recordEnqueue(); // 监控埋点
}
// 事件处理核心逻辑
private void processEvents() {
while (!Thread.currentThread().isInterrupted()) {
OrderEvent event = queue.poll(); // 无阻塞获取
if (event != null) {
try {
handleEvent(event);
} catch (Exception ex) {
handleFailure(event, ex); // 异常处理
}
} else {
// 队列空时自适应休眠(避免CPU空转)
sleepBackoff();
}
}
}
// 指数退避休眠(动态调节CPU使用率)
private void sleepBackoff() {
long delay = 1; // 初始1ms
while (queue.isEmpty() && delay < 100) {
try {
TimeUnit.MILLISECONDS.sleep(delay);
delay <<= 1; // 指数增加等待时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// 优雅关闭
public void shutdown() {
workers.shutdown();
while (!queue.isEmpty()) {
// 等待剩余任务处理完成
Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
}
}
}
需要注意的是,以下场景不适合。
- 阻塞需求,需要 take()阻塞等待。
- 有界队列
- 强一致性,要求精确的 size。
实现原理
以 JDK 17 源码为基线,ConcurrentLinkedQueue 是 Java 并发包中基于无锁算法实现的线程安全队列,专为高并发场景设计。其核心设计目标包括:
- 无阻塞操作:通过 CAS 实现非阻塞算法。
- 线性扩展能力:性能随 CPU 核心数增加而提升。
- 弱一致性:迭代器与 size() 方法返回近似值。
- 内存效率:每个元素仅需 24 字节存储开销。
ConcurrentLinkedQueue 的结构

ConcurrentLinkedQueue 由 head 节点和 tail 节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成,节点与节点之间就是通过这个 next 关联起来,从而组成一张链表结构的队列。
ConcurrentLinkedQueue 的节点都是 Node 类型的。
kotlin
static finalclass Node<E> {
volatile E item;
volatile Node<E> next;
Node(E item) {
ITEM.set(this, item);
}
Node() {}
void appendRelaxed(Node<E> next) {
NEXT.set(this, next);
}
boolean casItem(E cmp, E val) {
return ITEM.compareAndSet(this, cmp, val);
}
}
入队操作(offer)
流程图如下。

代码解析。
ini
public boolean offer(E e) {
final Node<E> newNode = new Node<E>(Objects.requireNonNull(e));
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
// CAS插入新节点
if (NEXT.compareAndSet(p, null, newNode)) {
// 惰性更新tail(允许失败)
if (p != t)
TAIL.weakCompareAndSet(this, t, newNode);
returntrue;
}
}
elseif (p == q) // 处理已移除节点
p = (t != (t = tail)) ? t : head;
else// 推进指针
p = (p != t && t != (t = tail)) ? t : q;
}
}
优化点:
weakCompareAndSet
减少内存屏障开销。- 允许尾指针最多滞后 log(n) 个节点。
- 通过
VarHandle
实现精确内存排序。
出队操作(poll)
核心机制:
- 两阶段出队:先标记 item 为 null,再更新 head。
- 头指针可能跳跃多个已消费节点。
- 自动清理无效节点。
流程图如下。

核心源码。
ini
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item;
if ((item = p.item) != null && p.casItem(item, null)) {
// 成功获取数据
if (p != h)
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
elseif ((q = p.next) == null) {
updateHead(h, p);
returnnull;
}
elseif (p == q)
// 继续循环
continue restartFromHead;
}
}
}
Java 7 种阻塞队列
Chaya:"什么是阻塞队列?"
阻塞队列,顾名思义,首先它是一个队列,线程 1 往阻塞队列中添加元素,而线程 2 从阻塞队列中移除元素。
- 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。
- 当阻塞队列是满时,从队列中添加元素的操作将会被阻塞。

JDK1.8 中的阻塞队列实现共有 7 个,分别是:
ArrayBlockingQueue
:基于数组的有界队列。LinkedBlockingQueue
:基于链表的无界队列(可以设置容量)。PriorityBlockingQueue
:基于二叉堆的无界优先级队列。DelayQueue
:基于PriorityBlockingQueue
的无界延迟队列。SynchronousQueue
:无容量的阻塞队列(Executors.newCachedThreadPool() 中使用的队列)。LinkedTransferQueue
:基于链表的无界队列。LinkedBlockingDeque
:一个由链表结构组成的双向阻塞队列。
阻塞队列核心接口
阻塞队列统一实现了BlockingQueue
接口,BlockingQueue
接口在java.util
包Queue
接口的基础上提供了put(e)
以及take()
两个阻塞方法。
除了阻塞功能,BlockingQueue
接口还定义了定时的offer
以及poll
,以及一次性移除方法drainTo
。
java
//插入元素,队列满后会抛出异常
boolean add(E e);
//移除元素,队列为空时会抛出异常
E remove();
//插入元素,成功反会true
boolean offer(E e);
//移除元素
E poll();
//插入元素,队列满后会阻塞
void put(E e) throws InterruptedException;
//移除元素,队列空后会阻塞
E take() throws InterruptedException;
//限时插入
boolean offer(E e, long timeout, TimeUnit unit)
//限时移除
E poll(long timeout, TimeUnit unit);
//获取所有元素到Collection中
int drainTo(Collection<? super E> c);
阻塞队列 6 大使用场景
Java 阻塞队列(BlockingQueue
)是并发编程中的核心工具,其线程安全 和阻塞特性使其在以下场景中发挥重要作用。
生产者-消费者模型(经典场景)
电商系统中,用户下单后需异步处理库存扣减、支付回调、物流通知等操作。
痛点:生产(下单)与消费(处理)速度不一致,需解耦并保证高吞吐。
scss
public class OrderProcessor {
// 生产级配置:建议队列大小为 CPU 核心数*2~4
privatestaticfinal BlockingQueue<Order> queue = new LinkedBlockingQueue<>(2048);
privatestaticfinal ExecutorService consumerPool = Executors.newFixedThreadPool(8);
// 生产者(Web 服务线程)
public void submitOrder(Order order) {
if (!queue.offer(order)) { // 队列满时快速失败
log.warn("Order queue overflow! Reject order: {}", order.getId());
thrownew ServiceException("系统繁忙,请稍后重试");
}
log.info("Order submitted: {}", order.getId());
}
// 消费者(后台线程池)
@PostConstruct
public void initConsumers() {
for (int i = 0; i < 8; i++) {
consumerPool.execute(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Order order = queue.take(); // 阻塞直到有订单
processOrder(order);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}
}
private void processOrder(Order order) {
// 扣减库存、支付回调等业务逻辑
}
}

线程池任务调度
场景问题
视频平台需将上传的视频转码为不同分辨率,任务具有突发性。
arduino
public class VideoTranscoder {
// 使用 PriorityBlockingQueue 确保 VIP 用户优先处理
privatestaticfinal BlockingQueue<TranscodeTask> queue =
new PriorityBlockingQueue<>(1000, Comparator.comparing(TranscodeTask::getPriority));
// 自定义线程池(核心线程数=CPU数, 最大线程数=CPU数*2)
privatestaticfinal ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, 8, 30, TimeUnit.SECONDS, queue
);
public void submitTranscodeTask(TranscodeTask task) {
executor.execute(() -> {
// 执行实际转码操作
process(task);
});
}
// 监控队列状态(生产环境建议接入 Prometheus)
public MonitorData getQueueStatus() {
returnnew MonitorData(
queue.size(),
executor.getActiveCount(),
queue.remainingCapacity()
);
}
}

生产级要点:
- 使用有界队列避免 OOM。
RejectedExecutionHandler
需配置合理拒绝策略。- 队列监控接入告警系统。
流量削峰
瞬时流量高峰可达平时 100 倍,数据库无法承受直接压力。
arduino
public class SeckillService {
// 队列容量=商品库存*2(内存可控)
privatefinal BlockingQueue<SeckillRequest> queue =
new ArrayBlockingQueue<>(20000);
// 异步消费队列
@Scheduled(fixedRate = 100)
public void processQueue() {
List<SeckillRequest> batch = new ArrayList<>(100);
queue.drainTo(batch, 100); // 批量取100条
if (!batch.isEmpty()) {
seckillDao.batchProcess(batch); // 批量写入数据库
}
}
public boolean trySeckill(SeckillRequest request) {
return queue.offer(request); // 非阻塞提交
}
}

生产级设计:
- 队列容量与数据库吞吐量匹配。
- 批量处理减少数据库压力。
- 前端配合显示排队状态。
延迟任务调度:订单超时关闭
需在订单创建 30 分钟后检查支付状态,未支付自动关闭。
typescript
public class OrderTimeoutChecker implements Runnable {
privatefinal DelayQueue<DelayedOrder> queue = new DelayQueue<>();
public void addOrder(Order order) {
queue.put(new DelayedOrder(order, 30, TimeUnit.MINUTES));
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
DelayedOrder order = queue.take(); // 阻塞直到有到期订单
checkPayment(order.getOrderId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
privatestaticclass DelayedOrder implements Delayed {
privatefinal Order order;
privatefinallong expireTime;
// 实现 getDelay() 和 compareTo()
}
}

生产级注意:
- 分布式场景需用 Redis/ZooKeeper 替代。
- 集群环境下需防重复处理。
- 添加 JVM 关闭钩子确保任务不丢失。
异步日志系统
需要记录详细业务日志但磁盘 I/O 不能影响主线程性能。
csharp
public class AsyncLogger {
privatestaticfinal BlockingQueue<LogEvent> queue =
new LinkedTransferQueue<>(); // 高吞吐无界队列
static {
// 守护线程消费日志
Thread loggerThread = new Thread(() -> {
while (true) {
try {
LogEvent event = queue.take();
writeToDisk(event);
} catch (InterruptedException e) {
// 优雅关闭处理
drainRemainingLogs();
break;
}
}
});
loggerThread.setDaemon(true);
loggerThread.start();
}
public static void log(LogEvent event) {
if (!queue.offer(event)) { // 防御性设计
fallbackLog(event);
}
}
}
线程池队列
线程池中活跃线程数达到 corePoolSize 时,线程池将会将后续的 task 提交到 BlockingQueue 中。
线程池的核心方法 ThreadPoolExecutor,用 BlockingQueue 存放任务的阻塞队列,被提交但尚未被执行的任务。
java
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
线程池在内部实际也是构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。

不同的线程池实现用的是不同的阻塞队列,newFixedThreadPool 和 newSingleThreadExecutor 用的是 LinkedBlockingQueue,newCachedThreadPool 用的是 SynchronousQueue。
各类队列对比和选型

生产选型建议:
- 网络请求缓冲 →
ArrayBlockingQueue
(可控内存) - 任务调度 →
PriorityBlockingQueue
(优先级控制) - 线程间直接通信 →
SynchronousQueue
- 磁盘 I/O 解耦 →
LinkedBlockingQueue
(吞吐优先)
性能优化要点
队列监控:
arduino
// 通过 JMX 暴露指标
public class QueueMonitor implements QueueMonitorMXBean {
private final BlockingQueue<?> queue;
public int getQueueSize() {
return queue.size();
}
// 注册到 MBeanServer...
}
拒绝策略(以线程池为例):
typescript
new ThreadPoolExecutor.CallerRunsPolicy() { // 生产推荐策略
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
log.warn("Task rejected, running in caller thread");
if (!e.isShutdown()) {
r.run();
}
}
}
动态扩缩容:
arduino
// 根据监控指标调整队列容量
public void adjustQueueCapacity(int newSize) {
if (queue instanceof ResizableBlockingQueue) {
((ResizableBlockingQueue) queue).setCapacity(newSize);
}
}
掌握了各种阻塞队列的使用场景,接下来我们深入拆解每个阻塞队列的实现原理。
ArrayBlockingQueue
ArrayBlockingQueue
是一个底层用数组实现的有界阻塞队列,有界是指他的容量大小是固定的,不能扩充容量,在初始化时就必须确定队列大小。
它通过可重入的独占锁ReentrantLock
来控制并发,Condition
来实现阻塞和通知唤醒。
结构概述
scala
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
final Object[] items; // 容器数组
int takeIndex; // 出队索引
int putIndex; // 入队索引
int count; // 排队个数
final ReentrantLock lock; // 全局锁
private final Condition notEmpty; // 出队条件队列
private final Condition notFull; // 入队条件队列
...
}
ArrayBlockingQueue
的结构如图所示:

ArrayBlockingQueue
的数组其实是一个逻辑上的环状结构,在添加、取出数据的时候,并没有像ArrayList
一样发生数组元素的移动(当然除了removeAt(final int removeIndex)
)。- 并且由
takeIndex
和putIndex
指示读写位置。 - 在读写的时候还有两个读写条件队列。
阻塞入队
阻塞入队 put
方法:
ini
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e);
final ReentrantLock lock = this.lock;
//获取独占锁
lock.lockInterruptibly();
try {
//如果队列已满则通过await阻塞put方法
while (count == items.length)
notFull.await();
//满足条件,插入元素,并唤醒因notEmpty等待的消费线程
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
//插入元素后将putIndex+1,当队列使用完后重置为0
if (++putIndex == items.length)
putIndex = 0;
count++;
//队列添加元素后唤醒因notEmpty等待的消费线程
notEmpty.signal();
}
阻塞出队
ini
//移除队列中的元素
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//获取独占锁
lock.lockInterruptibly();
try {
//如果队列已空则通过await阻塞take方法
while (count == 0)
notEmpty.await();
return dequeue(); //移除元素
} finally {
lock.unlock();
}
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
//移除元素后将takeIndex+1,当队列使用完后重置为0
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
//队列消费元素后唤醒因notFull等待的消费线程
notFull.signal();
return x;
}
队列满后通过notFull.await()
来阻塞生产者线程,消费元素后通过 notFull.signal()来唤醒阻塞的生产者线程。
队列为空后通过notEmpty.await()
来阻塞消费者线程,生产元素后通过notEmpty.signal()
唤醒阻塞的消费者线程。
drainTo
drainTo
方法可以一次性获取队列中所有的元素,它减少了锁定队列的次数,使用得当在某些场景下对性能有不错的提升。
ini
public int drainTo(Collection<? super E> c, int maxElements) {
checkNotNull(c);
if (c == this)
thrownew IllegalArgumentException();
if (maxElements <= 0)
return0;
final Object[] items = this.items;
final ReentrantLock lock = this.lock; //仅获取一次锁
lock.lock();
try {
int n = Math.min(maxElements, count); //获取队列中所有元素
int take = takeIndex;
int i = 0;
try {
while (i < n) {
@SuppressWarnings("unchecked")
E x = (E) items[take];
c.add(x); //循环插入元素
items[take] = null;
if (++take == items.length)
take = 0;
i++;
}
return n;
} finally {
// Restore invariants even if c.add() threw
if (i > 0) {
count -= i;
takeIndex = take;
if (itrs != null) {
if (count == 0)
itrs.queueIsEmpty();
elseif (i > take)
itrs.takeIndexWrapped();
}
for (; i > 0 && lock.hasWaiters(notFull); i--)
notFull.signal(); //唤醒等待的生产者线程
}
}
} finally {
lock.unlock();
}
}
LinkedBlockingQueue
LinkedBlockingQueue
是一个底层用单向链表实现的有界阻塞队列,和ArrayBlockingQueue
一样,采用ReentrantLock
来控制并发,不同的是它使用了两个独占锁来控制消费和生产。
如果不是特殊业务,LinkedBlockingQueue
使用时,切记要定义容量 new LinkedBlockingQueue(capacity)
,防止过度膨胀。
结构概述
ini
public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
// 默认 Integer.MAX_VALUE
privatefinalint capacity;
// 容量
privatefinal AtomicInteger count = new AtomicInteger();
// 头结点 head.item == null
transient Node<E> head;
// 尾节点 last.next == null
privatetransient Node<E> last;
// take锁,出队锁,只有take,poll方法会持有
privatefinal ReentrantLock takeLock = new ReentrantLock();
// 出队等待条件
// 当队列无元素时,take锁会阻塞在notEmpty条件上,等待其它线程唤醒
privatefinal Condition notEmpty = takeLock.newCondition();
// 入队锁,只有put,offer会持有
privatefinal ReentrantLock putLock = new ReentrantLock();
// 入队等待条件
// 当队列满了时,put锁会会阻塞在notFull上,等待其它线程唤醒
privatefinal Condition notFull = putLock.newCondition();
// 基于链表实现,肯定要有结点类,典型的单链表结构
staticclass Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
}
LinkedBlockingQueue
的结构如图所示:

如图所示,
LinkedBlockingQueue
其实就是一个简单的单向链表,其中头部元素的数据为空,尾部元素的 next 为空。- 因为读写都有竞争,所以在头部和尾部分别有一把锁;同时还有对应的两个条件队列。
put 和 take 方法
ini
public void put(E e) throws InterruptedException {
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
//因为使用了双锁,需要使用AtomicInteger计算元素总量,避免并发计算不准确
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
//队列已满,阻塞生产线程
notFull.await();
}
//插入元素到队列尾部
enqueue(node);
//count + 1
c = count.getAndIncrement();
//如果+1后队列还未满,通过其他生产线程继续生产
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
//只有当之前是空时,消费队列才会阻塞,否则是不需要通知的
if (c == 0)
signalNotEmpty();
}
private void enqueue(Node<E> node) {
//将新元素添加到链表末尾,然后将last指向尾部元素
last = last.next = node;
}
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();
//count - 1
c = count.getAndDecrement();
// 通知其他等待的消费线程继续消费
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
//只有当前队列是满的,生产队列才会阻塞,否则是不需要通知的
signalNotFull();
return x;
}
//消费队列头部的下一个元素,同时将新头部置空
private E dequeue() {
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;
}
可以看到LinkedBlockingQueue
通过takeLock
和putLock
两个锁来控制生产和消费,互不干扰,只要队列未满,生产线程可以一直生产,只要队列不为空,消费线程可以一直消费,不会相互因为独占锁而阻塞。
Chaya:"为什么 ArrayBlockingQueue 中不使用双锁来实现队列的生产和消费呢?"
我的理解是 ArrayBlockingQueue 也能使用双锁来实现功能,但由于它底层使用了数组这种简单结构,相当于一个共享变量,如果通过两个锁,需要更加精确的锁控制。
LinkedBlockingQueue
不存在这个问题,链表这种数据结构头尾节点都相对独立,存储上也不连续,双锁控制不存在复杂性。
PriorityBlockingQueue
PriorityBlockingQueue
是一个底层由数组实现的无界队列,并带有排序功能,同样采用ReentrantLock
来控制并发。
由于是无界的,所以插入元素时不会阻塞,没有队列满的状态,只有队列为空的状态。
PriorityBlockingQueue 具有以下特性:
- 基于优先级排序 :元素按自然顺序(
Comparable
)或自定义Comparator
排序。 - 线程安全 :通过
ReentrantLock
保证并发操作的安全性。 - 动态扩容:底层是数组实现的二叉堆,容量不足时自动扩容。
- 阻塞操作 :
take()
在队列为空时阻塞;put()
不会阻塞(因为队列无界)。
适用场景
例子中,创建了一个优先级阻塞队列,用于存储和检索PriorityTask
对象,这些对象根据它们的优先级进行排序,client 代码会向队列中添加任务,并从队列中检索并处理优先级最高的任务。
arduino
// 定义一个具有优先级的任务类
class PriorityTask implements Comparable<PriorityTask>{
privateint priority;
private String name;
public PriorityTask(int priority, String name) {
this.priority = priority;
this.name = name;
}
@Override
public int compareTo(PriorityTask o) {
if (this.priority < o.priority) {
return -1;
} elseif (this.priority > o.priority) {
return1;
} else {
return0;
}
}
@Override
public String toString() {
return"PriorityTask{" +
"priority=" + priority +
", name='" + name + ''' +
'}';
}
}
publicclass PriorityBlockingQueueExample {
public static void main(String[] args) throws InterruptedException {
// 创建一个优先级阻塞队列,使用PriorityTask类的自然顺序进行排序
PriorityBlockingQueue<PriorityTask> queue = new PriorityBlockingQueue<>();
// 添加任务到队列
queue.put(new PriorityTask(3, "Task 3"));
queue.put(new PriorityTask(1, "Task 1"));
queue.put(new PriorityTask(2, "Task 2"));
// 从队列中取出并打印任务,优先级高的先出队
while (!queue.isEmpty()) {
PriorityTask task = queue.take();
System.out.println("Processing: " + task);
}
}
}
在这个示例中,定义了一个名为 PriorityTask
的类,它实现了 Comparable
接口,并且重写了 compareTo
方法来定义优先级规则。
队列中的元素将根据这个规则自动排序,从而保证优先级高的任务先被处理。
实现原理
PriorityBlockingQueue
内部通过 数组 维护一个 最小二叉堆(默认),堆顶元素始终是优先级最高的(最小元素)。 数组下标关系:
- 父节点:
parent = (childIndex - 1) / 2
- 左子节点:
leftChild = parent * 2 + 1
- 右子节点:
rightChild = parent * 2 + 2
关键字段
java
// 存储元素的数组
private transient Object[] queue;
// 元素数量
private transientint size;
// 排序规则(为 null 时使用自然排序)
private transient Comparator<? super E> comparator;
// 保证线程安全的锁
private final ReentrantLock lock = new ReentrantLock();
// 非空条件变量(用于 take() 阻塞)
private final Condition notEmpty = lock.newCondition();
入队源码
scss
public boolean offer(E e) {
if (e == null)
thrownew NullPointerException();
// 首先获取锁对象。
final ReentrantLock lock = this.lock;
// 只有一个线程操作入队和出队动作。
lock.lock();
// n代表数组的实际存储内容的大小
// cap代表队列的整体大小,也就是数组的长度。
int n, cap;
Object[] array;
// 如果数组实际长度大于等于数组的长度时,需要进行扩容操作。
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap);
try {
// 如果用户指定比较器,则使用用户指定的比较器来进行比较,如果没有则使用默认比较器。
Comparator<? super E> cmp = comparator;
if (cmp == null)
// 进行上浮操作。
siftUpComparable(n, e, array);
else
// 进行上浮操作。
siftUpUsingComparator(n, e, array, cmp);
// 实际长度增加1,由于有且仅有一个线程操作队列,所以这里并没有使用原子性操作。
size = n + 1;
// 通知等待的线程,队列已经有数据,可以获取数据。
notEmpty.signal();
} finally {
// 解锁操作。
lock.unlock();
}
// 返回操作成功。
returntrue;
}
关键步骤
- 加锁:确保线程安全。
- 扩容检查 :若数组已满,调用
tryGrow()
扩容(通常扩容 50%)。 - 堆上浮:将新元素插入数组末尾,逐步与父节点比较并交换,直到满足堆性质。
- 唤醒消费者 :通知可能阻塞的
take()
线程。
出队
ini
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
while ( (result = dequeue()) == null)
notEmpty.await();
} finally {
lock.unlock();
}
return result;
}
private E dequeue() {
// 数组的元素的个数。
int n = size - 1;
// 如果数组中不存在元素则直接返回null。
if (n < 0)
returnnull;
else {
// 获取队列数组。
Object[] array = queue;
// 将第一个元素也就是二叉堆的根结点堆顶元素作为返回结果。
E result = (E) array[0];
// 获取数组中最后一个元素。
E x = (E) array[n];
// 将最后一个元素设置为null。
array[n] = null;
Comparator<? super E> cmp = comparator;
if (cmp == null)
// 进行下沉操作。
siftDownComparable(0, x, array, n);
else
// 进行下沉操作。
siftDownUsingComparator(0, x, array, n, cmp);
// 实际元素大小减少1.
size = n;
// 返回结果。
return result;
}
}
关键步骤
- 加锁:确保线程安全。
- 取出堆顶:返回堆顶元素(优先级最高)。
- 堆下沉:将末尾元素移到堆顶,逐步与子节点比较并交换,直到满足堆性质。
DelayQueue
DelayQueue 也是一个无界队列,它是在PriorityQueue
基础上实现的,先按延迟优先级排序,延迟时间短的排在前面。
和PriorityBlockingQueue
相似,底层也是数组,采用一个ReentrantLock
来控制并发。
由于是无界的,所以插入元素时不会阻塞,没有队列满的状态。
ini
private finaltransient ReentrantLock lock = new ReentrantLock();
privatefinal PriorityQueue<E> q = new PriorityQueue<E>();//优先级队列
public void put(E e) {
offer(e);
}
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e); //插入元素到优先级队列
if (q.peek() == e) { //如果插入的元素在队列头部
leader = null;
available.signal(); //通知消费线程
}
returntrue;
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek(); //获取头部元素
if (first == null)
available.await(); //空队列阻塞
else {
long delay = first.getDelay(NANOSECONDS); //检查元素是否延迟到期
if (delay <= 0)
return q.poll(); //到期则弹出元素
first = null; // don't retain ref while waiting
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay); //阻塞未到期的时间
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
SynchronousQueue
SynchronousQueue
相比较之前的 4 个队列就比较特殊了,它是一个没有容量的队列,也就是说它内部时不会对数据进行存储,每进行一次 put 之后必须要进行一次 take,否则相同线程继续 put 会阻塞。
这种特性很适合做一些传递性的工作,一个线程生产,一个线程消费。
这里只对它的非公平实现下的 take 和 put 方法做下简单分析:
scss
//非公平情况下调用内部类TransferStack的transfer方法put
public void put(E e) throws InterruptedException {
if (e == null) thrownew NullPointerException();
if (transferer.transfer(e, false, 0) == null) {
Thread.interrupted();
thrownew InterruptedException();
}
}
//非公平情况下调用内部类TransferStack的transfer方法take
public E take() throws InterruptedException {
E e = transferer.transfer(null, false, 0);
if (e != null)
return e;
Thread.interrupted();
thrownew InterruptedException();
}
//具体的put以及take方法,只有E的区别,通过E来区别REQUEST还是DATA模式
E transfer(E e, boolean timed, long nanos) {
SNode s = null; // constructed/reused as needed
int mode = (e == null) ? REQUEST : DATA;
for (;;) {
SNode h = head;
//栈无元素或者元素和插入的元素模式相匹配,也就是说都是插入元素
if (h == null || h.mode == mode) {
//有时间限制并且超时
if (timed && nanos <= 0) {
if (h != null && h.isCancelled())
casHead(h, h.next); // 重新设置头节点
else
returnnull;
}
//未超时cas操作尝试设置头节点
elseif (casHead(h, s = snode(s, e, h, mode))) {
//自旋一段时间后未消费元素则挂起put线程
SNode m = awaitFulfill(s, timed, nanos);
if (m == s) { // wait was cancelled
clean(s);
returnnull;
}
if ((h = head) != null && h.next == s)
casHead(h, s.next); // help s's fulfiller
return (E) ((mode == REQUEST) ? m.item : s.item);
}
}
//栈不为空并且和头节点模式不匹配,存在元素则消费元素并重新设置head节点
elseif (!isFulfilling(h.mode)) { // try to fulfill
if (h.isCancelled()) // already cancelled
casHead(h, h.next); // pop and retry
elseif (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
for (;;) { // loop until matched or waiters disappear
SNode m = s.next; // m is s's match
if (m == null) { // all waiters are gone
casHead(s, null); // pop fulfill node
s = null; // use new node next time
break; // restart main loop
}
SNode mn = m.next;
if (m.tryMatch(s)) {
casHead(s, mn); // pop both s and m
return (E) ((mode == REQUEST) ? m.item : s.item);
} else // lost match
s.casNext(m, mn); // help unlink
}
}
}
//节点正在匹配阶段
else { // help a fulfiller
SNode m = h.next; // m is h's match
if (m == null) // waiter is gone
casHead(h, null); // pop fulfilling node
else {
SNode mn = m.next;
if (m.tryMatch(h)) // help match
casHead(h, mn); // pop both h and m
else // lost match
h.casNext(m, mn); // help unlink
}
}
}
}
//先自旋后挂起的核心方法
SNode awaitFulfill(SNode s, boolean timed, long nanos) {
finallong deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
//计算自旋的次数
int spins = (shouldSpin(s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
if (w.isInterrupted())
s.tryCancel();
SNode m = s.match;
//匹配成功过返回节点
if (m != null)
return m;
//超时控制
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel();
continue;
}
}
//自旋检查,是否进行下一次自旋
if (spins > 0)
spins = shouldSpin(s) ? (spins-1) : 0;
elseif (s.waiter == null)
s.waiter = w; // establish waiter so can park next iter
elseif (!timed)
LockSupport.park(this); //在这里挂起线程
elseif (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
代码非常复杂,这里说下我所理解的核心逻辑。
代码中可以看到put
以及take
方法都是通过调用transfer
方法来实现的,然后通过参数mode
来区别,在生产元素时如果是同一个线程多次put
则会采取自旋的方式多次尝试put
元素,可能自旋过程中元素会被消费,这样可以及时put
,降低线程挂起的性能损耗,高吞吐量的核心也在这里.
消费线程一样,空栈时也会先自旋,自旋失败然后通过线程的LockSupport.park
方法挂起。
LinkedTransferQueue
LinkedTransferQueue
是一个由链表结构组成的无界阻塞 TransferQueue
队列。
LinkedTransferQueue
采用一种预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为 null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时发现有一个元素为 null 的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回。我们称这种节点操作为"匹配"方式。
队列实现了 TransferQueue
接口重写了 tryTransfer 和
transfer方法,这组方法和
SynchronousQueue` 公平模式的队列类似,具有匹配的功能.。
LinkedBlockingDeque
LinkedBlockingDeque
是一个由链表结构组成的双向阻塞队列。
所谓双向队列指的你可以从队列的两端插入和移出元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。
相比其他的阻塞队列,LinkedBlockingDeque
多了 addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast
等方法,以 First 单词结尾的方法,表示插入,获取(peek)或移除双端队列的第一个元素。
以 Last 单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。
另外插入方法 add 等同于 addLast,移除方法 remove 等效于 removeFirst。
在初始化 LinkedBlockingDeque 时可以设置容量防止其过渡膨胀,默认容量也是 Integer.MAX_VALUE。
另外双向阻塞队列可以运用在"工作窃取"模式中。