并发容器
HashMap 的线程不安全问题
HashMap 是使用非常多的容器,也是 Map 最主要的实现类之一,但其自身不具备线程安全的特点,可从多种情况体现。
源码分析
HashMap 的 put 方法源码:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.recordAccess(this);
return oldValue;
}
}
//modCount++ 是一个复合操作
modCount++;
addEntry(hash, key, value, i);
return null;
}
modCount++ 是典型的"i++"操作,属于线程不安全的"运行结果错误"情况。i++ 并非原子操作,其执行分三步,每步之间都可能被打断:
- 读取;
- 增加;
- 保存。
线程不安全发生过程如下:

线程 1 先拿到 i=1,进行 i+1 操作但未保存,随后被切换走;线程 2 执行相同 i++ 操作,拿到的 i 仍为 1(线程 1 结果未保存,线程 2 不可见)。线程 2 完成 +1 后切回线程 1,保存结果 2;再切回线程 2 保存 i=2。两个线程各执行一次 +1,最终却只得到 i=2 而非 i=3,导致数据结果错误。
从源码角度足以证明 HashMap 线程不安全。多个线程同时调用 put() 时可能算错 modCount 值(以上为 Java 7 源码;Java 8 的 put 调用 putVal 方法,内部同样有 ++modCount 语句,原理相同)。
实验:扩容期间取出的值不准确
HashMap 默认容量不大,持续添加数据会在合适时机扩容。扩容期间新建空数组,用旧项填充;填充过程中若有线程取值,可能取到 null 而不是原值。演示代码如下:
public class HashMapNotSafe {
public static void main(String[] args) {
final Map<Integer, String> map = new HashMap<>();
final Integer targetKey = 0b1111_1111_1111_1111; // 65 535
final String targetValue = "v";
map.put(targetKey, targetValue);
new Thread(() -> {
IntStream.range(0, targetKey).forEach(key -> map.put(key, "someValue"));
}).start();
while (true) {
if (null == map.get(targetKey)) {
throw new RuntimeException("HashMap is not thread safe.");
}
}
}
}
代码建立 HashMap,key 取二进制 1111_1111_1111_1111(十进制 65535),目的是让扩容回填数据时不要太快,便于捕捉错误;value 取非 null 的 "v"。新线程通过 IntStream(0 到 65535 左闭右开)不断往 map 添加 key,value 统一为 "someValue"。主线程进入 while 循环,反复检测 targetKey 对应的值是否为 "v";若取到 null 则抛出 RuntimeException。
运行结果:
Exception in thread "main" java.lang.RuntimeException: HashMap is not thread safe.
at lesson29.HashMapNotSafe.main(HashMapNotSafe.java:25)
程序很快抛出 RuntimeException,证明取到的是 null 而非 "v",HashMap 线程不安全。
除上述例子外,HashMap 还有其他线程不安全情况:
同时 put 碰撞导致数据丢失
多个线程同时 put,且 key 发生碰撞(hash 计算出的 bucket 位置相同),两个线程同时判断该位置为空可写入,则两个不同 value 添加到同一位置,最终只保留一个数据,丢失一个数据。
可见性问题无法保证
线程安全需保证可见性,即一个线程操作容器时其他线程能感知。HashMap 无法保证:线程 1 给某 key 放入新值,线程 2 获取该 key 时可能看到、也可能看不到此次更改。
死循环造成 CPU 100%
HashMap 扩容逻辑会反转散列桶中的节点顺序。多线程同时扩容时,若两个线程同时反转,可能形成链表循环(A 指向 B,B 指回 A),之后获取 key 对应的 value 时遍历链表永不结束,导致 CPU 100%。
综上,HashMap 线程不安全,多线程场景应避免使用。Collections.synchronizedMap(new HashMap()) 虽线程安全但效率低下(内部大量使用 synchronized,多线程不能同时操作)。推荐使用线程安全且性能较好的 ConcurrentHashMap。
ConcurrentHashMap 在 Java7 与 8 的差异
Java 8 对 ConcurrentHashMap 进行了大幅升级。Java 7 的 Segment 设计思想仍具参考价值。二者结构、原理、性能差异如下。
Java 7 版本的 ConcurrentHashMap
Java 7 版本 ConcurrentHashMap 结构示意图:

ConcurrentHashMap 内部进行 Segment 分段,Segment 继承 ReentrantLock,可视为一把锁,各 Segment 之间独立上锁、互不影响。相比 Hashtable 每次操作锁住整个对象,并发效率大幅提高。
每个 Segment 底层数据结构与 HashMap 类似,为数组和链表组成的拉链法结构。默认有 0~15 共 16 个 Segment,最多同时支持 16 个线程并发操作(分布在不同 Segment 上)。16 为默认值,可在初始化时设置其他值,但一旦确认初始化后不可扩容。
Java 8 版本的 ConcurrentHashMap
Java 8 几乎完全重写 ConcurrentHashMap,代码量从 Java 7 的 1000 多行增至 6000 多行。整体结构示意图:

图中的节点有三种类型:
- 空位:当前还没有元素填充。
- 拉链法结构:每个槽先填入第一个节点,后续相同 Hash 值用链表往后延伸。
- 红黑树结构:Java 7 的 ConcurrentHashMap 所没有的结构。
当链表长度大于阈值(默认 8)且满足一定容量要求时,链表转化为红黑树,提高查找性能。红黑树是每个节点带红色或黑色属性的二叉查找树,本质是对 BST 的平衡策略,查找效率高、自动平衡。
由于自平衡特点(左右子树高度几乎一致),查找性能近似二分查找,时间复杂度 O(log(n));链表最坏情况需遍历整个链表,时间复杂度 O(n),节点越多 O(log(n)) 优势越明显。
红黑树其他特点:
- 每个节点为红色或黑色,根节点永远为黑色。
- 红色节点不能连续,红色节点的子节点和父节点都不能是红色。
- 从任一节点到其每个叶子节点的路径包含相同数量的黑色节点。
这些规则保证较高查找效率。Java 8 引入红黑树是为了避免极端情况下冲突链表过长导致查询慢,保证极端情况下查询效率在 O(log(n))。
分析 Java 8 版本的 ConcurrentHashMap 的重要源码
Java 7 已过时,重点分析 Java 8 版本源码。
Node 节点
最基础的内部存储结构 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 是 key-value 形式,value 用 volatile 修饰保证可见性,内部有 next 指针指向下一个节点,形成链表结构。
put 方法源码分析
put 方法核心是 putVal 方法,源码及注释分析:
final V putVal(K key, V value, boolean onlyIfAbsent) {
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;
//如果数组是空的,就进行初始化
if (tab == null || (n = tab.length) == 0) {
tab = initTable();
}
// 找该 hash 值对应的数组下标
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//如果该位置是空的,就用 CAS 的方式放入新值
if (casTabAt(tab, i, null,
new Node<K, V>(hash, key, value, null))) {
break;
}
}
//hash值等于 MOVED 代表在扩容
else if ((fh = f.hash) == MOVED) {
tab = helpTransfer(tab, f);
}
//槽点上是有值的情况
else {
V oldVal = null;
//用 synchronized 锁住当前槽点,保证并发安全
synchronized (f) {
if (tabAt(tab, i) == f) {
//如果是链表的形式
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;
if (!onlyIfAbsent) {
e.val = value;
}
break;
}
Node<K, V> pred = e;
//到了链表的尾部也没有发现该 key,说明之前不存在,就把新值添加到链表的最后
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;
}
}
}
}
}
if (binCount != 0) {
//检查是否满足条件并把链表转换为红黑树的形式,默认的 TREEIFY_THRESHOLD 阈值是 8
if (binCount >= TREEIFY_THRESHOLD) {
treeifyBin(tab, i);
}
//putVal 的返回是添加前的旧值,所以返回 oldVal
if (oldVal != null) {
return oldVal;
}
break;
}
}
}
addCount(1L, binCount);
return null;
}
putVal 方法根据当前槽点的不同状态(未初始化、空、扩容、链表、红黑树)做出不同处理。
get 方法源码分析
get 方法源码注释分析:
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//计算 hash 值
int h = spread(key.hashCode());
//如果整个数组是空的,或者当前槽点的数据是空的,说明 key 对应的 value 不存在,直接返回 null
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,说明是红黑树或者正在扩容,就用对应的 find 方法来查找
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;
}
get 过程总结:
- 计算 Hash 值,据此找到对应的槽点;
- 若数组为空或该位置为 null,直接返回 null;
- 若该位置节点刚好是需要的,直接返回该节点的值;
- 若该位置节点是红黑树或正在扩容,用 find 方法继续查找;
- 否则为链表,遍历链表查找。
对比Java7 和Java8 的异同和优缺点
数据结构
Java 7 采用 Segment 分段锁,Java 8 采用数组 + 链表 + 红黑树,二者差异很大。


并发度
Java 7 中每个 Segment 独立加锁,最大并发个数为 Segment 的个数,默认 16。Java 8 锁粒度更细,理想情况下 table 数组元素个数(数组长度)即为支持并发的最大个数,并发度提高。
保证并发安全的原理
Java 7 采用 Segment 分段锁保证安全,Segment 继承自 ReentrantLock。Java 8 放弃 Segment 设计,采用 Node + CAS + synchronized 保证线程安全。
遇到 Hash 碰撞
Java 7 在 Hash 冲突时使用拉链法(链表形式)。Java 8 先使用拉链法,在链表长度超过阈值时转换为红黑树提高查找效率。
查询时间复杂度
Java 7 遍历链表时间复杂度 O(n),n 为链表长度。Java 8 遍历红黑树时间复杂度 O(log(n)),n 为树的节点个数。
Map 桶转红黑树的阈值
JDK 1.8 的 HashMap 和 ConcurrentHashMap:往 Map 放元素时计算 hash 值,第 1 个 value 占用一个桶(槽点)位置,后续计算结果落到同一桶时用链表形式往后延长,即"拉链法",如图所示:

图中有的桶为空(第 4 个),有的只有一个元素(1、3、6),有的为拉链法结构(第 2、5 个)。
当链表长度大于或等于阈值(默认 8),且满足容量大于或等于 MIN_TREEIFY_CAPACITY(默认 64)时,链表转换为红黑树。后续若因删除或调整大小使红黑树节点小于或等于 6 个,恢复为链表形态。
HashMap 结构示意图:

图中有空槽点、拉链、红黑树三种形态。
为什么需要转换
每次遍历链表平均查找时间复杂度 O(n),n 为链表长度。红黑树有自平衡特点,可防止不平衡,始终将查找时间复杂度控制在 O(log(n))。链表短时 O(n) 与 O(log(n)) 差别不大;链表变长后区别显现。为提升查找性能,需将链表转化为红黑树。
为什么不一开始就用红黑树
JDK 源码注释对此作了说明:
Because TreeNodes are about twice the size of regular nodes,
use them only when bins contain enough nodes to warrant use
(see TREEIFY_THRESHOLD). And when they become too small (due
removal or resizing) they are converted back to plain bins.
单个 TreeNode 占用的空间约为普通 Node 的两倍,只有包含足够多 Nodes 时才转成 TreeNodes,是否足够由 TREEIFY_THRESHOLD 决定。桶中节点数因移除或 resize 变少后,又变回普通链表以节省空间。
默认链表长度达 8 转红黑树,降到 6 转换回去,体现时间和空间平衡思想。链表短时空间占用少、查询时间无大问题;链表变长后用红黑树保证查询效率。阈值默认取 8,源码对选择 8 的解释如下:
In usages with well-distributed user hashCodes, tree bins
are rarely used. Ideally, under random hashCodes, the
frequency of nodes in bins follows a Poisson distribution
(http://en.wikipedia.org/wiki/Poisson_distribution) with a
parameter of about 0.5 on average for the default resizing
threshold of 0.75, although with a large variance because
of resizing granularity. Ignoring variance, the expected
occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
factorial(k)). The first values are:
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
more: less than 1 in ten million
若 hashCode 分布良好(hash 计算结果离散好),红黑树很少被用到。理想情况下链表长度符合泊松分布,各长度命中概率依次递减,长度为 8 时概率仅为 0.00000006(小于千万分之一)。Map 通常不会存储这么多数据,所以一般不会发生链表向红黑树的转换。
HashMap 决定元素落桶与对象 hashCode 有关,JDK 无法阻止用户实现自己的哈希算法。若故意使哈希算法不均匀,例如:
@Override
public int hashCode() {
return 1;
}
hashCode 始终返回 1,容易导致 HashMap 链表变长。验证代码:
public class HashMapDemo {
public static void main(String[] args) {
HashMap map = new HashMap<HashMapDemo,Integer>(1);
for (int i = 0; i < 1000; i++) {
HashMapDemo hashMapDemo1 = new HashMapDemo();
map.put(hashMapDemo1, null);
}
System.out.println("运行结束");
}
@Override
public int hashCode() {
return 1;
}
}
运行时若通过 debug 暂停在 System.out.println("运行结束"),观察 map 内节点已变为 TreeNode 而非通常的 Node,说明内部已转为红黑树。

链表长度超过 8 转为红黑树的设计,主要是防止用户实现不好的哈希算法导致链表过长、查询效率低,此时转红黑树是一种保底策略,保证极端情况下查询效率。
hash 算法正常时链表不会很长,红黑树不会带来明显查询优势,反而增加空间负担。所以选择概率小于千万分之一的长度 8 作为转换默认阈值。
开发中发现 HashMap 或 ConcurrentHashMap 内部出现红黑树结构,通常说明哈希算法出了问题,需检查是否实现了效果不好的 hashCode 方法并改进,以减少冲突。
ConcurrentHashMap 与 Hashtable 的区别
HashMap 线程不安全,ConcurrentHashMap 和 Hashtable 均为线程安全,二者在以下四个角度存在不同。
出现的版本不同
Hashtable 在 JDK 1.0 时存在,JDK 1.2 版本实现 Map 接口,成为集合框架成员。ConcurrentHashMap 在 JDK 1.5 才出现。后出现的类通常是对前面类的优化,二者在实现方式和性能上存在较大不同。
实现线程安全的方式不同
Hashtable 通过 synchronized 关键字实现并发安全,以 clear() 方法为例:
public synchronized void clear() {
Entry<?,?> tab[] = table;
modCount++;
for (int index = tab.length; --index >= 0; )
tab[index] = null;
count = 0;
}
clear() 被 synchronized 修饰,其他方法如 put、get、size 同样被 synchronized 修饰。Hashtable 因几乎所有方法都被 synchronized 修饰而线程安全。Collections.SynchronizedMap(new HashMap()) 的原理与 Hashtable 类似,也是利用 synchronized。
ConcurrentHashMap 实现原理不同,Java 8 结构示意图:

ConcurrentHashMap 实现线程安全的原理(见第 30 节详述和源码分析)是利用 CAS + synchronized + Node 节点方式,与 Hashtable 完全依赖 synchronized 的方式有很大不同。
性能不同
线程安全实现方式不同导致性能不同。线程数量增加时 Hashtable 性能急剧下降,因为每次修改需锁住整个对象,其他线程期间不能操作,还带来额外上下文切换开销,吞吐量甚至不如单线程。ConcurrentHashMap 上锁时仅对一部分上锁而非全部,多线程吞吐量通常大于单线程,并发效率比 Hashtable 提高很多。
迭代时修改的不同
Hashtable(包括 HashMap)不允许在迭代期间修改内容,否则抛出 ConcurrentModificationException。其原理是检测 modCount 变量,迭代器的 next() 方法:
public T next() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
return nextElement();
}
next() 先判断 modCount 是否等于 expectedModCount。expectedModCount 在迭代器生成时产生且不改变,代表当前 Hashtable 被修改的次数;每次调用 addEntry()、remove()、rehash() 等方法都会修改 modCount。若迭代期间修改内容,迭代器在 next() 时发现 modCount 不等于 expectedModCount,抛出 ConcurrentModificationException。
所以 Hashtable 不允许迭代期间修改内容。相反,ConcurrentHashMap 即便迭代期间修改内容,也不会抛出 ConcurrentModificationException。
综上,ConcurrentHashMap 与 Hashtable 虽都线程安全,但在出现版本、实现线程安全方式、性能、迭代时是否支持修改等方面均有较大不同。并发场景使用 ConcurrentHashMap 最合适,Hashtable 已不推荐使用。
CopyOnWriteArrayList 的特点
在 CopyOnWriteArrayList 之前已有 ArrayList、LinkedList,以及线程安全的 Vector 和 Collections.synchronizedList()。Vector 的 size 和 get 方法:
public synchronized int size() {
return elementCount;
}
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
Vector 用 synchronized 保证线程安全,锁粒度大(方法级别),并发量高时易竞争,并发效率较低,与 Hashtable 类似。此外上述 List 迭代期间不允许编辑,否则抛出 ConcurrentModificationException。
从 JDK 1.5 起,Java 并发包提供使用 CopyOnWrite 机制实现的 CopyOnWriteArrayList 作为主要并发 List。CopyOnWrite 并发集合还包括 CopyOnWriteArraySet,其底层用 CopyOnWriteArrayList 实现。
适用场景
-
读操作尽可能快,写操作慢一些也可以:读操作远多于写操作的场景(如系统级信息只加载或修改很少次数,但被系统内所有模块频繁访问),希望读操作尽可能快,写慢一些没关系。
-
读多写少:黑名单是最典型场景。搜索网站用户输入关键字搜索,不允许搜索的关键字放在黑名单,黑名单不需要实时更新(如每晚更新一次),用户搜索时检查关键字是否在黑名单。此读多写少场景适合用 CopyOnWrite 集合。
读写规则
-
读写锁的规则:读写锁思想是"读读共享、其他都互斥"(写写互斥、读写互斥、写读互斥)。读操作不修改原数据,并发读无安全问题;写操作危险,写发生时不允许读操作加入,也不允许第二个写线程加入。
-
对读写锁规则的升级:CopyOnWriteArrayList 为将读取性能发挥到极致,读取完全不用加锁,且写入不会阻塞读取操作(可边写边读),只有写入与写入之间需要同步。写发生时允许读同时发生,读性能大幅提升。
特点
- CopyOnWrite 的含义:CopyOnWrite 指容器需要修改时不直接修改当前容器,而是先将当前容器 Copy 出新副本,修改新容器,修改完成后将原容器引用指向新容器,完成整个修改过程。
好处是利用"不变性"原理:容器每次修改都创建新副本,旧容器不可变、线程安全,无需进一步同步。可对 CopyOnWrite 容器并发读而不加锁(当前容器不添加元素也不修改)。所有修改操作(add、set 等)通过创建底层数组新副本实现,体现读写分离思想,读和写使用不同容器。
- 迭代期间允许修改集合内容:ArrayList 迭代期间修改集合内容会抛出 ConcurrentModificationException。ArrayList 源码 ListItr 的 next 方法中有 checkForComodification 方法:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
该方法检查 modCount 是否等于 expectedModCount。modCount 保存修改次数,每次调用 add、remove、trimToSize 等方法时增加;expectedModCount 是迭代器变量,创建迭代器时初始化并记录当时 modCount。迭代期间若 modCount 与 expectedModCount 不一致,说明集合被修改,抛出异常。
与 ArrayList 不同,CopyOnWriteArrayList 的迭代器迭代时若数组内容被修改,不会抛 ConcurrentModificationException,因为迭代器仍使用旧数组,只是迭代内容可能已过时。演示代码:
/**
* 描述: 演示CopyOnWriteArrayList迭代期间可以修改集合的内容
*/
public class CopyOnWriteArrayListDemo {
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(new Integer[]{1, 2, 3});
System.out.println(list); //[1, 2, 3]
//Get iterator 1
Iterator<Integer> itr1 = list.iterator();
//Add one element and verify list is updated
list.add(4);
System.out.println(list); //[1, 2, 3, 4]
//Get iterator 2
Iterator<Integer> itr2 = list.iterator();
System.out.println("====Verify Iterator 1 content====");
itr1.forEachRemaining(System.out::println); //1,2,3
System.out.println("====Verify Iterator 2 content====");
itr2.forEachRemaining(System.out::println); //1,2,3,4
}
}
代码创建初始值为 [1, 2, 3] 的 CopyOnWriteArrayList,创建 itr1 迭代器后调用 list.add(4) 添加元素,List 变为 [1, 2, 3, 4],再创建 itr2 迭代器。运行结果:
[1, 2, 3]
[1, 2, 3, 4]
====Verify Iterator 1 content====
1
2
3
====Verify Iterator 2 content====
1
2
3
4
两个迭代器打印内容不同:itr1 为 [1, 2, 3],itr2 为 [1, 2, 3, 4]。虽然打印时机都在第四个元素添加后,但创建时机不同。迭代器 1 创建时 List 只有三个元素,后续任何修改对它无感知。
结论:CopyOnWriteArrayList 迭代器一旦建立后,向原对象新增元素,迭代器既不会显示变更,也不会报错,与 ArrayList 有很大区别。
缺点
以下缺点不仅针对 CopyOnWriteArrayList,也适用于其他 CopyOnWrite 容器:
-
内存占用问题:写时复制机制使写操作时内存同时驻扎两个对象的内存,占用额外内存空间。
-
在元素较多或复杂的情况下,复制的开销很大:复制不仅占用双倍内存,还消耗 CPU 等资源,降低整体性能。
-
数据一致性问题:修改先作用于副本,此次修改对其它线程并非实时可见,只有修改完成后才体现。若希望写入数据马上被其他线程看到,CopyOnWrite 容器不适用。
源码分析
- 数据结构
/** 可重入锁对象 */
final transient ReentrantLock lock = new ReentrantLock();
/** CopyOnWriteArrayList底层由数组实现,volatile修饰,保证数组的可见性 */
private transient volatile Object[] array;
/**
* 得到数组
*/
final Object[] getArray() {
return array;
}
/**
* 设置数组
*/
final void setArray(Object[] a) {
array = a;
}
/**
* 初始化CopyOnWriteArrayList相当于初始化数组
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
类中有 ReentrantLock 锁,保证修改操作线程安全。命名为 array 的 Object[] 数组被 volatile 修饰,保证数组可见性,是存储元素的数组。从 getArray()、setArray() 及构造方法可见 CopyOnWriteArrayList 底层用数组实现。
- add 方法
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;
// 将volatile Object[] array 的指向替换成新数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
add 方法流程:先用 ReentrantLock 加锁;获取原数组长度和元素(getArray 得到 elements 并保存 length);用 Arrays.copyOf 复制新数组;把新元素添加到新数组;用 setArray(newElements) 将 volatile Object[] array 指向替换为新数组;最后在 finally 中解锁。
总结:添加时上锁,复制新数组,增加操作在新数组上完成,将 array 指向新数组,最后解锁。
上述步骤实现 CopyOnWrite 思想:写操作在容器拷贝上进行,读取数据时不锁 list。拷贝操作过程中有新的读线程进来,读到的还是旧数据(此时引用尚未更改)。
读操作相关代码(get 两个重载和 getArray):
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
return (E) a[index];
}
get 相关操作不加锁,保证读取高速。
- 迭代器 COWIterator 类
迭代器有两个重要属性:Object[] snapshot 和 int cursor。snapshot 代表数组快照(创建迭代器时刻的数组情况),cursor 是迭代器游标。构造方法:
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
迭代器构建时把当时 elements 赋值给 snapshot,之后所有操作基于 snapshot 数组:
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
next 方法返回 snapshot 对象,后续原数组被修改,snapshot 既不感知也不受影响,执行迭代操作无需加锁,也不会抛异常。迭代器返回结果与创建迭代器时内容一致。
CopyOnWriteArrayList 要点总结:诞生前的 Vector 和 Collections.synchronizedList() 特点;适用场景、读写规则;两个特点(写时复制、迭代期间允许修改集合内容);三个缺点(内存占用、元素多或复杂时复制开销大、数据一致性);重要源码解析。
阻塞队列
阻塞队列的作用
阻塞队列即 BlockingQueue,是一个接口:
public interface BlockingQueue<E> extends Queue<E>{...}
BlockingQueue 继承 Queue 接口,是队列的一种。Queue 和 BlockingQueue 都在 Java 5 中加入。
BlockingQueue 线程安全,可用线程安全队列优雅解决业务自身的线程安全问题。生产者和消费者模式中,生产者往队列添加元素,消费者从队列取出元素,如图所示:

图中左侧三个生产者线程把生产结果放到中间阻塞队列,右侧三个消费者从队列取出内容处理。因阻塞队列线程安全,生产者和消费者都可多线程,不会发生线程安全问题。
队列可安全地从一线程向另一线程传递数据,生产者和消费者直接使用线程安全队列即可,无需自己考虑更多线程安全问题。考虑锁等线程安全问题的重任从开发方转移到"队列"上,降低开发难度和工作量。
队列还能起隔离作用。如银行转账程序,生产者线程无需关心转账逻辑,只需把转账任务(账户和金额等)放入队列;银行类从队列取出待执行任务,通过自身方法完成转账。这实现具体任务与执行任务类之间的解耦,任务放在阻塞队列中,放任务的线程无法直接访问银行转账实现对象,实现隔离、提高安全性。
主要并发队列关系图

Java 提供的线程安全队列(并发队列)分为阻塞队列和非阻塞队列两大类。
阻塞队列典型例子是 BlockingQueue 接口的实现类,主要有 6 种:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、DelayQueue、PriorityBlockingQueue 和 LinkedTransferQueue,各自特点见第 36 节。
非阻塞并发队列典型例子是 ConcurrentLinkedQueue,不会让线程阻塞,利用 CAS 保证线程安全。
可根据需要自由选取阻塞队列或非阻塞队列满足业务需求。
与 Queue 关系紧密的 Deque 接口继承自 Queue:
public interface Deque<E> extends Queue<E> {//...}
Deque(双端队列,double-ended-queue 缩写)从头和尾都能添加和删除元素;普通 Queue 只能一端进、另一端出。这是 Deque 与 Queue 的不同,其他性质类似。
阻塞队列的特点
阻塞队列区别于其他类型队列的最主要特点是"阻塞"。阻塞功能使生产者和消费者两端能力平衡,任一端速度过快时,阻塞队列会将其速度降下来。实现阻塞最重要的两个方法是 take 方法和 put 方法。
take 方法
take 方法获取并移除队列头结点,队列有数据时可正常移除;执行 take 时队列无数据则阻塞,直到队列有数据;队列有数据后立刻解除阻塞并取到数据。过程如图所示:

put 方法
put 方法插入元素时,若队列未满则正常插入;若队列已满则无法继续插入,阻塞直到队列有空闲空间;后续队列有空闲空间(如消费者消费一个元素)则解除阻塞,把数据添加到队列。过程如图所示:

以上阻塞和解除阻塞均由 BlockingQueue 完成,无需自己处理。
是否有界(容量有多大)
阻塞队列容量分为有界和无界两种。
无界队列可容纳非常多元素,如 LinkedBlockingQueue 上限是 Integer.MAX_VALUE(约 2 的 31 次方),可近似认为无限容量,几乎无法装满。
有界队列如 ArrayBlockingQueue 容量满了不会扩容,一旦满了就无法再放数据。
阻塞队列要点:作用(线程安全、解耦、隔离)、并发队列分类(阻塞/非阻塞,阻塞队列有 6 种常见实现)、特点(take 方法、put 方法、是否有界)。
阻塞队列的常用方法
BlockingQueue 最常用的与添加、删除相关的 8 个方法可分为三组,每组都与添加、移除元素相关。三组方法功能类似,区别仅在特殊情况:队列满无法添加或队列空无法移除时,不同组方法处理方式不同:
- 抛出异常:add、remove、element
- 返回结果但不抛出异常:offer、poll、peek
- 阻塞:put、take
第一组:add、remove、element
add 方法
add 往队列添加一个元素,队列满时抛异常提示队列已满。示例:
private static void addTest() {
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(2);
blockingQueue.add(1);
blockingQueue.add(1);
blockingQueue.add(1);
}
创建容量为 2 的 BlockingQueue 放 3 个值,添加第三个值时得到异常:
Exception in thread "main" java.lang.IllegalStateException:Queue full
remove 方法
remove 删除元素,队列为空时无元素可删,抛出异常。示例:
private static void removeTest() {
ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(2);
blockingQueue.add(1);
blockingQueue.add(1);
blockingQueue.remove();
blockingQueue.remove();
blockingQueue.remove();
}
往容量 2 的队列放入 2 个元素,删除 3 个。删除前两个正常,删除第三个时队列已空,抛异常:
Exception in thread "main" java.util.NoSuchElementException
element 方法
element 返回队列头部节点但不删除。操作空队列时抛 NoSuchElementException(与 remove 相同)。示例:
private static void elementTest() {
ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(2);
blockingQueue.element();
}
新建容量 2 的 ArrayBlockingQueue 直接调用 element,因队列为空,得到异常:
Exception in thread "main" java.util.NoSuchElementException
第二组:offer、poll、peek
第二组比第一组友好,当队列满无法添加或队列空无法删除时给出提示而非抛异常。
offer 方法
offer 插入元素,用返回值提示是否成功。成功返回 true;队列满时继续调用不抛异常,返回 false。示例:
private static void offerTest() {
ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(2);
System.out.println(blockingQueue.offer(1));
System.out.println(blockingQueue.offer(1));
System.out.println(blockingQueue.offer(1));
}
容量 2 的队列调用三次 offer,运行结果:
true
true
false
前两次添加成功,第三次超过最大容量返回 false。
poll 方法
poll 对应第一组 remove,移除并返回头节点;队列空时返回 null 提示。因无法区分返回 null 是提示还是元素,不允许往队列插入 null。示例:
private static void pollTest() {
ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(3);
blockingQueue.offer(1);
blockingQueue.offer(2);
blockingQueue.offer(3);
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
}
容量 3 的队列放入 3 个元素,四次调用 poll,运行结果:
1
2
3
null
前三次成功并返回 1、2、3(先进先出),第四次返回 null 代表无元素可移除。
peek 方法
peek 对应第一组 element,返回头元素但不删除;队列空时返回 null。示例:
private static void peekTest() {
ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(2);
System.out.println(blockingQueue.peek());
}
运行结果:
null
新建空队列直接调用 peek,返回 null。
带超时时间的 offer 和 poll
offer 和 poll 都有带超时时间的重载方法:
offer(E e, long timeout, TimeUnit unit)
三个参数为元素、超时时长、时间单位。通常插入成功返回 true;队列满导致插入不成功时,等待指定超时时间,超时仍未插入成功则返回 false。
poll(long timeout, TimeUnit unit)
带时间参数 poll 与 offer 类似:能移除则立即返回节点内容;队列空则等待指定时间,超时仍无元素可移除返回 null。
第三组:put、take
put 和 take 是阻塞队列最大特色的方法(见第 34 节)。
put 方法
put 插入元素,队列未满时正常插入;队列已满时既不立即返回 false 也不抛异常,而是让插入线程陷入阻塞,直到队列有空闲空间,此时队列解除之前线程的阻塞,并把元素添加进去。

take 方法
take 获取并移除头结点,队列有数据时正常取出并删除;执行 take 时队列无数据则阻塞,直到队列有数据;队列有数据后立刻解除阻塞并取到数据。

总结
三组方法特点:
- 第一组(add、remove、element):无法正常执行时抛异常。
- 第二组(offer、poll、peek):无法正常执行时不抛异常,用返回值提示运行失败。
- 第三组(put、take):遇到特殊情况时让线程陷入阻塞,等到可运行再继续。
8 种方法总结如下表:

此表可清晰理清 8 个方法间的关系。
常见的阻塞队列
BlockingQueue 接口的实现类都放在 J.U.C 包中。常见实现类包括 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue、DelayQueue。
ArrayBlockingQueue
ArrayBlockingQueue 是最典型的有界队列,内部用数组存储元素,利用 ReentrantLock 实现线程安全。
创建时需指定容量,之后不可扩容,构造函数可指定是否公平:
ArrayBlockingQueue(int capacity, boolean fair)
第一个参数是容量,第二个参数是是否公平。与 ReentrantLock 一样,ArrayBlockingQueue 设为非公平时存在插队可能;设为公平时等待最长时间的线程优先处理,其他线程不允许插队,但公平策略带来一定性能损耗(非公平吞吐量通常高于公平)。
LinkedBlockingQueue
内部用链表实现的 BlockingQueue。不指定初始容量时默认容量为整型最大值 Integer.MAX_VALUE,因数值非常大、通常不可能放入这么多数据,LinkedBlockingQueue 也被称为无界队列,代表几乎没有界限。
SynchronousQueue

SynchronousQueue 容量为 0,没有地方暂存元素,每次取数据都要先阻塞直到有数据被放入;每次放数据也会阻塞直到有消费者来取。
SynchronousQueue 的容量是 0 而非 1,因为它不需要持有元素,只做直接传递(direct handoff)。每次传递时把元素直接从生产者传给消费者,无需存储,运用得当效率很高。
容量为 0 使 SynchronousQueue 的许多方法实现特殊。peek 方法永远返回 null:
public E peek() {
return null;
}
peek 含义是取出头结点,但 SynchronousQueue 容量为 0,连头结点都没有,peek 无意义,始终返回 null。同理 element 始终抛 NoSuchElementException。
size 方法始终返回 0:
public int size() {
return 0;
}
isEmpty 方法始终返回 true:
public boolean isEmpty() {
return true;
}
因它始终都是空的。
PriorityBlockingQueue
ArrayBlockingQueue 和 LinkedBlockingQueue 采用先进先出顺序排序,需要自定义排序时使用 PriorityBlockingQueue。
PriorityBlockingQueue 是支持优先级的无界阻塞队列,可通过自定义类实现 compareTo() 指定排序规则,或初始化时通过构造器参数 Comparator 指定。插入队列的对象必须是可比较大小的(Comparable),否则抛 ClassCastException。
take 方法在队列为空时阻塞;因是无界队列且自动扩容,队列永远不会满,put 方法永不阻塞,添加操作始终成功。因此它的成员变量只有一个 Condition:
private final Condition notEmpty;
这与 ArrayBlockingQueue 拥有两个 Condition(notEmpty 和 notFull)形成对比。PriorityBlockingQueue 不需要 notFull,因为它永远不会满。
DelayQueue
DelayQueue 具有"延迟"功能,可设定队列中任务延迟多久后执行(如 10 秒后执行),大量用于"30 分钟后未付款自动取消订单"等延迟执行场景。
它是无界队列,放入的元素必须实现 Delayed 接口,Delayed 接口继承 Comparable,自然拥有比较和排序能力:
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
Delayed 接口继承自 Comparable,有一个需实现的方法 getDelay,返回"还剩下多长的延迟时间才会被执行",若返回 0 或负数代表任务已过期。
元素根据延迟时间长短被放到队列不同位置,越靠近队列头代表越早过期。
DelayQueue 内部使用 PriorityQueue 能力排序而非从头编写,可复用已有功能,减少开发量,避免"重复造轮子"。
ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue、DelayQueue 这些常见阻塞队列的特点已逐一介绍。
阻塞与非阻塞队列的并发安全原理
以 ArrayBlockingQueue 为例分析阻塞队列线程安全原理,再分析非阻塞队列的并发安全原理。
ArrayBlockingQueue 源码分析
ArrayBlockingQueue 有几个重要属性:
final Object[] items;
int takeIndex;
int putIndex;
int count;
Object[] items:核心的用于存储元素的数组。takeIndex、putIndex:标明下一次读取和写入位置的两个变量。count:计数,记录队列中的元素个数。
另外还有三个变量:
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
lock:一个 ReentrantLock。notEmpty、notFull:两个由 ReentrantLock 产生的 Condition。
这三个变量是实现线程安全最核心的工具。ArrayBlockingQueue 利用 ReentrantLock 和两个 Condition 实现并发安全,真正执行读写操作前需先获取锁。
分析最重要的 put 方法:
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
put 方法先用 checkNotNull 检查插入元素是否 null;非 null 后用 lock.lockInterruptibly() 上锁,该方法在获取锁的同时可响应中断(见第 23 节),这正是阻塞队列调用 put 方法在尝试获取锁但未拿到锁期间可响应中断的底层原因。
随后是经典 try-finally 代码块,finally 中解锁,try 中有 while 循环检查队列是否已满(count 是否等于数组长度)。已满则等待,有空余后才调用 enqueue 让元素进入队列,最后 unlock 解锁。
该代码与第 5 节用 Condition 实现生产者/消费者模式时写的 put 方法几乎一致:
public void put(Object o) throws InterruptedException {
lock.lock();
try {
while (queue.size() == max) {
notFull.await();
}
queue.add(o);
notEmpty.signalAll();
} finally {
lock.unlock();
}
}
两个 put 方法几乎一模一样。用 Condition 实现生产者/消费者模式,本质上就是实现了简易版 BlockingQueue。对比两个 put 方法实现可加深对 Condition 的理解。
与 ArrayBlockingQueue 类似,其他阻塞队列(LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue、DelayedWorkQueue 等)内部也利用 ReentrantLock 保证线程安全,只是细节有差异。例如 LinkedBlockingQueue 内部有两把锁分别锁队列头和尾,比共用一把锁效率更高,但总体思想类似。
非阻塞队列ConcurrentLinkedQueue
ConcurrentLinkedQueue 使用链表作为数据结构,关键方法 offer 的源码:
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
// p is last node
if (p.casNext(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become "live".
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)
// We have fallen off list. If tail is unchanged, it
// will also be off-list, in which case we need to
// jump to head, from which all live nodes are always
// reachable. Else the new tail is a better bet.
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
p = (p != t && t != (t = tail)) ? t : q;
}
}
从整体代码结构看,检查空判断后是一个大的 for 循环(明显的死循环)。循环中有个关键方法 p.casNext,正是利用 CAS 操作;死循环配合 CAS 是典型的乐观锁思想。p.casNext 方法实现:
boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
这里运用 UNSAFE.compareAndSwapObject 完成 CAS 操作,compareAndSwapObject 是 native 方法,最终利用 CPU 的 CAS 指令保证不可中断。
非阻塞队列 ConcurrentLinkedQueue 使用 CAS 非阻塞算法 + 不停重试实现线程安全,适合不需要阻塞功能且并发不是特别剧烈的场景。
总结
- 阻塞队列最主要是利用 ReentrantLock 及它的 Condition 实现并发安全。
- 非阻塞队列利用 CAS 方法实现线程安全。
参考:https://javadoop.com/post/java-concurrent-queue
阻塞队列的选择
线程池在选择阻塞队列上已率先做出表率。不同种类的线程池根据自身特点选择适合自己的阻塞队列,可借鉴其经验总结选取规则。
线程池对于阻塞队列的选择

表格左侧是线程池,右侧为对应阻塞队列,5 种线程池只对应 3 种阻塞队列,逐一介绍:
-
FixedThreadPool(SingleThreadExecutor 同理)选取 LinkedBlockingQueue:LinkedBlockingQueue 不同于 ArrayBlockingQueue(容量有限),其链表长度默认可无限延长。FixedThreadPool 线程数固定,任务激增时无法增加线程处理 Task,需要像 LinkedBlockingQueue 这样无容量上限的 Queue 存储未处理 Task。若所有 corePoolSize 线程都在忙,新任务进入阻塞队列等待;队列无容量上限、永不填满,保证 FixedThreadPool 和 SingleThreadExecutor 不拒绝新任务提交、不丢失数据。
-
CachedThreadPool 选取 SynchronousQueue:为避免新任务被拒绝,CachedThreadPool 选择无限制的 maximumPoolSize,线程最大数量无限、线程数不受限制,因此不需要额外空间存储 Task,每个任务都可通过新建线程处理。SynchronousQueue 直接把任务交给线程、无需另外保存,效率更高,故 CachedThreadPool 使用 SynchronousQueue。
-
ScheduledThreadPool(SingleThreadScheduledExecutor 同理)选取延迟队列:ScheduledThreadPool 使用 DelayedWorkQueue。延迟队列不是先进先出,而是按延迟时间长短排序,下一个即将执行的任务排到最前面。
例如队列中先放延迟 10 分钟执行的任务,再放延迟 10 秒执行的任务。非延迟队列按先进先出规则,延迟 10 分钟的任务先放置、在最前面;但使用阻塞队列时按延迟时间长短排放位置,第二个放置的延迟 10 秒任务反而排在延迟 10 分钟任务前面(其执行时间更早)。
选择延迟队列的原因是 ScheduledThreadPool 处理基于时间执行的 Task,而延迟队列能把 Task 按执行时间先后排序,正是所需功能。
ArrayBlockingQueue
除线程池选择的 3 种阻塞队列外,ArrayBlockingQueue 也常用于手动创建的线程池。
ArrayBlockingQueue 内部用数组实现,新建对象时要求传入容量值,后期不能扩容,最大特点是容量有限且固定。使用 ArrayBlockingQueue 且设置合理最大线程数的线程池,任务队列放满后若线程数也达最大值,线程池按规则拒绝新任务提交,不会无限增加任务或线程数导致内存不足,有效防止资源耗尽。
归纳
通常可从 5 个角度考虑选择合适阻塞队列:
-
功能:是否需要阻塞队列排序,如优先级排序、延迟执行等。有需要则选择类似 PriorityBlockingQueue 之类有排序能力的阻塞队列。
-
容量:是否有存储要求,还是只需"直接传递"。前文几种队列中:ArrayBlockingQueue 容量固定;LinkedBlockingQueue 默认容量无限;SynchronousQueue 没有任何容量;DelayQueue 容量固定为 Integer.MAX_VALUE。不同队列容量千差万别,需根据任务数量推算出合适容量从而选取合适 BlockingQueue。
-
能否扩容:业务可能有高峰期、低谷期,初始时不一定能准确估计队列大小。若需动态扩容,不能选 ArrayBlockingQueue(容量创建时确定、无法扩容);PriorityBlockingQueue 即使指定初始容量,后续需要也可自动扩容。
-
内存结构:ArrayBlockingQueue 内部结构是"数组",LinkedBlockingQueue 内部用链表实现。ArrayBlockingQueue 没有链表所需的"节点",空间利用率更高。对性能有要求可从内存结构角度考虑。
-
性能:LinkedBlockingQueue 拥有两把锁、操作粒度更细,并发程度高时相对只有一把锁的 ArrayBlockingQueue 性能更好。SynchronousQueue 性能往往优于其他实现(只需"直接传递"、无需存储),需要直接传递场景可优先考虑。
选取规则归纳:回顾线程池对阻塞队列的选取规则,看到 ArrayBlockingQueue 特点,通常从功能、容量、能否扩容、内存结构、性能这 5 个角度结合业务选取最适合的阻塞队列。
Java 21 并发容器新内容
SequencedCollection / SequencedMap(JEP 431)
Java 21 引入 SequencedCollection(有序集合)与 SequencedMap(有序 Map)接口,统一了支持头尾双向访问的集合 API:
// 旧版:反向遍历需手动转换
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (int i = list.size() - 1; i >= 0; i--) { /* ... */ }
// Java 21:直接调用 reversed()
SequencedCollection<String> seq = list;
seq.getFirst(); // "a"
seq.getLast(); // "c"
seq.reversed(); // [c, b, a]ConcurrentHashMap 虽然不是 SequencedMap,但 ConcurrentSkipListMap 实现了 NavigableMap/SequencedMap,支持有序并发访问。
并发容器 vs 虚拟线程
Java 21 中,虚拟线程的高并发 IO 场景下,共享可变状态仍须使用并发容器;但如果任务无共享状态(如纯 IO 请求),可减少对容器的依赖。
| 场景 | 推荐方案 |
|---|---|
| 高频读写共享 Map | ConcurrentHashMap |
| 高频追加读多写少 | CopyOnWriteArrayList |
| 高并发有序 KV | ConcurrentSkipListMap(支持 SequencedMap) |
| 任务队列 | 虚拟线程 + 无队列直接调度 |
版本差异(旧版 → Java 21)
| 特性 | 旧版(Java 7/8) | Java 21 |
|---|---|---|
| 有序集合 API | 分散,无统一接口 | SequencedCollection/SequencedMap(JEP 431) |
| ConcurrentHashMap | JDK 8 起 CAS + synchronized 桶 | 不变,仍是首选并发 Map |
| 阻塞队列 | 7 种实现 | 不变;新增队列场景可配合虚拟线程 |
| 头尾访问 | 需手动转换 | getFirst/getLast/reversed 直接支持 |