我们复习了很多,日常敲代码感觉是不是不一样了已经,渐入佳境、登堂入室!咱们已经聊了 ConcurrentHashMap这辆"跑车"(CAS + 分段/节点锁),但现实世界的交通场景千变万化:
- 有时候你需要排队上车(生产者 - 消费者)。
- 有时候车上全是乘客,几乎没人下车(读多写少)。
- 有时候你需要按顺序到站(有序 Map)。
如果选错了交通工具(比如让几万人挤一辆跑车,或者让赶时间的人坐大巴),系统就会拥堵(性能低下)甚至瘫痪(死锁/OOM)
今天让我们拆解 BlockingQueue 、CopyOnWriteArrayList、ConcurrentSkipListMap,从底层源码到选型策略,构建一个接近完美的并发交通网【得意 努力】
第一章:BlockingQueue ------ "智能地铁系统" (生产者 - 消费者)
BlockingQueue 是一个线程安全 的队列,保证安全就是比较慢:核心特性是阻塞 (Blocking):
- 队列为空时 :消费者线程尝试取元素,会被阻塞,直到有元素入队。
- 队列已满时 :生产者线程尝试存元素,会被阻塞,直到有空间腾出。
比喻 :这就是地铁 ,站台容量有限(队列有界)
- 人满了,外面的人(生产者)必须在闸机口等着(阻塞),不能硬挤。
- 车走了,里面的人(消费者)必须等着下一站**(阻塞),不能跳下去跑。
- 完美解决 :生产者和消费者速度不匹配的问题,无需手动
wait/notify。
四大金刚实现及选型
1. ArrayBlockingQueue (ABQ) ------ "固定车厢的地铁"
- 底层结构 :数组 + ReentrantLock + Condition
- 数组,长度固定:有界:创建时必须指定容量,满了就是满了,不能扩容,上面没人走不了;put时必须阻塞等待消费者腾出来位置!
- 公平性可选 :构造函数可传
fair=true,保证先排队的先上车(FIFO),但性能略低 - 内存连续:数组结构,CPU 缓存友好(Cache Locality),但在高并发下锁竞争可能略大
适用场景 :对内存敏感,需要严格控制队列大小,防止 OOM 的场景
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E> {
final Object[] items; // 数组
int takeIndex, putIndex; // 头尾指针
final ReentrantLock lock;
private final Condition notEmpty; // 消费者等待条件
private final Condition notFull; // 生产者等待条件
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) // 队列为空,等待
notEmpty.await();
return dequeue(); // 出队
} finally {
lock.unlock();
}
}
public void put(E e) throws InterruptedException {
// ... 类似逻辑,满时 await notFull
}
}
2. LinkedBlockingQueue (LBQ) ------ "无限延伸的列车"
底层结构 :链表 + 两把锁 (putLock 和 takeLock) 在上面put和take还是两个指针哈哈
- 可选有界 :默认容量
Integer.MAX_VALUE(相当于无界,小心 OOM!),也可指定容量 - 读写分离锁:入队和出队使用不同的锁,并发度通常比 ABQ 高(因为入队和出队操作互不干扰)
- 内存不连续:链表节点分散,缓存命中率略低
适用场景:吞吐量要求高,能接受潜在内存风险(或设置了合理上限)的场景。咱们Tomcat 的线程池默认就用它!
public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
// 节点内部类
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
// 容量上限
private final int capacity;
// 当前元素个数 (AtomicInteger 保证可见性和原子计数)
private final AtomicInteger count = new AtomicInteger();
// 核心:两把锁!
// putLock 专门保护入队操作 (tail)
private final ReentrantLock putLock = new ReentrantLock();
// takeLock 专门保护出队操作 (head)
private final ReentrantLock takeLock = new ReentrantLock();
// 核心:两个条件变量
// notEmpty: 当队列空时,消费者等待此信号
private final Condition notEmpty = takeLock.newCondition();
// notFull: 当队列满时,生产者等待此信号
private final Condition notFull = putLock.newCondition();
// 头尾指针 (仅在持有对应锁时修改)
transient Node<E> head;
private transient Node<E> last;
// --- 入队方法 (put) ---
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly(); // 1. 获取入队锁
try {
// 2. 如果队列满了,等待 notFull 信号
while (count.get() == capacity) {
notFull.await();
}
// 3. 执行入队 (enqueue)
enqueue(node);
// 4. 原子增加计数,并获取旧值
c = count.getAndIncrement();
// 5. 关键优化:如果加 1 后还是没满 (c + 1 < capacity),说明还有空间,唤醒其他等待的生产者
// 注意:这里只唤醒一个,避免惊群效应
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
// 6. 如果加 1 之前是 0 (c == 0),说明刚才队列是空的,现在有了,需要唤醒消费者
if (c == 0)
signalNotEmpty();
}
private void enqueue(Node<E> node) {
last = last.next = node; // 链表追加
//last.next=node; 旧尾节点的next指针指向新节点
//last=node; 更新last引用,指向新的尾节点
}
// --- 出队方法 (take) ---
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly(); // 1. 获取出队锁
try {
// 2. 如果队列空了,等待 notEmpty 信号
while (count.get() == 0) {
notEmpty.await();
}
// 3. 执行出队 (dequeue)
x = dequeue();
// 4. 原子减少计数
c = count.getAndDecrement();
// 5. 关键优化:如果减 1 后还大于 0,说明还有货,唤醒其他消费者
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
// 6. 如果减 1 之前等于容量 (c == capacity),说明刚才满了,现在有空位了,唤醒生产者
if (c == capacity)
signalNotFull();
return x;
}
//出队逻辑:移除头节点,返回数据,并帮助垃圾回收
private E dequeue() {
Node<E> h = head;//获取当前的头节点 (注意:LBQ 的头节点通常是一个 dummy 空节点)
Node<E> first = h.next;//获取真正的第一个元素 (头节点的下一个)
h.next = h; //help GC,断开旧节点的引用 关键优化
head = first;//移动头指针:让新的第一个元素成为新的 dummy 头节点
E x = first.item;//取出数据
first.item = null;//关键gc优化,清空原第一个节点的数据引用
return x;
}
// 辅助方法:在持有 putLock 的情况下唤醒 takeLock 等待的线程
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
}
警告 :如果使用默认构造函数(无界),在生产者速度 > 消费者速度时,内存会无限增长直到 OOM。生产环境务必指定容量
3. PriorityBlockingQueue ------ "VIP 通道"
底层结构 :二叉堆 (Binary Heap) + ReentrantLock,优先级调度 ,而不是流量控制
-
堆操作 + 单一锁 + 非阻塞 put
-
无界队列: 动态扩容的数组 (
Object[] queue),没有固定容量上限, 当数组满了,它会像ArrayList一样自动扩容 (grow):**Arrays.copyOf**换一个更大的数组! -
put(E e):因为可以无限扩容,所以理论上永远有空间放新元素。既然总有空间,就不需要让生产者线程等待(阻塞)。它只会因为内存不足抛出OutOfMemoryError,而不会阻塞 -
take():如果堆里没有元素(size == 0),消费者必须等待,直到有任务进来,所以它会阻塞 -
有序 :出队顺序不是 FIFO,而是根据元素的自然排序 或 Comparator 决定优先级
-
非阻塞 :
take()在空时会阻塞,但put()永远不会阻塞(因为无界)public class PriorityBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E> {
// 默认初始容量 private static final int DEFAULT_INITIAL_CAPACITY = 11; // 存储元素的数组 (动态扩容) private Object[] queue; private int size; // 核心:只有一把锁,控制所有修改操作 private final ReentrantLock lock; // 核心:只有一个条件变量,供消费者等待 private final Condition notEmpty; // 用于比较优先级的 Comparator,或者元素自然排序 private final Comparator<? super E> comparator; public PriorityBlockingQueue() { this(DEFAULT_INITIAL_CAPACITY, null); } public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { // 1. 如果为空,一直等待 while (size == 0) notEmpty.await(); // 2. 取出堆顶元素 (index 0) E x = (E) queue[0]; // 3. 将最后一个元素移到堆顶,然后下沉 (siftDown) 维护堆性质 E end = (E) queue[size - 1];//取出堆顶最后一个元素,暂存为 end queue[size - 1] = null; // 帮助 GC size--;//堆大小减1 if (size > 0)//如果堆中有元素,进行下沉操作 siftDown(0, end); return x; } finally { lock.unlock(); } } public void put(E e) { // 重点:直接调用 offer,因为永远不会失败(除非 OOM) offer(e); } public boolean offer(E e) { if (e == null) throw new NullPointerException(); final ReentrantLock lock = this.lock; lock.lock(); try { // 1. 检查容量,如果不够,自动扩容 (grow) int i = size; if (i >= queue.length) grow(i + 1); // 2. 放入数组末尾 size = i + 1; if (i == 0) queue[0] = e; else // 3. 上浮 (siftUp) 维护堆性质 siftUp(i, e, comparator); // 4. 唤醒一个等待的消费者 notEmpty.signal(); } finally { lock.unlock(); } return true; } // 扩容逻辑:类似 ArrayList,每次扩大 1 倍 (如果小于 64) 或 1.5 倍 private void grow(int minCapacity) { int oldCapacity = queue.length; int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1)); if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); queue = Arrays.copyOf(queue, newCapacity); }}
- 无界扩容 :
offer/put中没有while(size == capacity)的等待逻辑,只有grow()。 - 堆维护 :利用
siftUp(插入) 和siftDown(删除) 保证queue[0]始终是最小(或最大)元素。 - 单一锁:因为读写都涉及数组结构调整(扩容或堆平衡),无法像 LBQ 那样分离锁,所以性能在高并发写入时不如 LBQ,但读取(take)依然高效
适用场景:任务调度,紧急任务优先处理(如:报警日志优先于普通日志)
任务总量可控 或消费能力足够强
4. DelayQueue ------ "定时发车专列"
底层结构 :PriorityQueue + ReentrantLock
-
无界
-
延时 :元素必须实现
Delayed接口。只有当元素的延迟时间到期后,才能被取出 -
用途:缓存过期清理、定时任务调度、订单超时取消
class DelayedTask implements Delayed {
private long expireTime;
// 实现 compareTo 和 getDelay 方法
public long getDelay(TimeUnit unit) {
long diff = expireTime - System.currentTimeMillis();
return unit.convert(diff, TimeUnit.MILLISECONDS);
}
}
// 使用
DelayQueue<DelayedTask> queue = new DelayQueue<>();
queue.put(new DelayedTask(5000)); // 5 秒后才能取出来
| 需求 | 推荐容器 | 理由 |
|---|---|---|
| 严格限制内存,防止 OOM | ArrayBlockingQueue |
强制有界,内存紧凑 |
| 高吞吐,读写并发高 | LinkedBlockingQueue (带容量) |
读写分离锁,性能更好 |
| 任务有优先级 | PriorityBlockingQueue |
自动排序,VIP 优先 |
| 延时任务/超时处理 | DelayQueue |
基于时间轮询,自动到期 |
第二章:CopyOnWriteArrayList (COW) ------ "读多写少的大巴"
CopyOnWriteArrayList 是一个线程安全的 List,其核心思想是:写时复制 (Copy-On-Write)
比喻 :这是一辆老式大巴。
- 乘客上车(读):随时可以上,不需要检票(无锁),速度极快。
- 司机改路线(写) :
- 司机不能直接在行驶的车上改路线(会撞车)。
- 司机把全车人复制到一辆新车上。
- 在新车上修改路线。
- 修改完后,把车站的牌子指向新车。
- 后续的新乘客直接上新车,旧车上的乘客坐完这一趟就下车(旧对象被 GC)。
底层原理:读写分离 + 最终一致性
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
//核心:volatile 修饰的数组,保证可见性
private transient volatile Object[] array;
public E get(int index) {
//读操作:完全无锁!直接访问数组
return get(getArray(), index);
}
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 1. 复制数组 (System.arraycopy) -> 开销大!
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 2. 在新数组上修改
newElements[len] = e;
// 3. 原子替换引用 (volatile 写)
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
}
- ✅ 优点 :
- 读性能极高:无锁,适合高并发读------随便读。
- 迭代安全 :遍历时不会抛出
ConcurrentModificationException,因为迭代器操作的是旧数组快照。
- ❌ 缺点 :
- 写性能差 :每次写都要复制整个数组,时间复杂度 O(N) ;如果列表很大,写入会导致 CPU 飙高和频繁 GC。
- 内存占用高:写入瞬间,内存中同时存在两份数组!!!
- 数据一致性弱 :只能保证最终一致性。读取时可能读到旧数据(刚写完还没切换引用时,其他线程可能还在读旧数组)。
适用场景(黄金法则)
"读多写少" 且 "写操作不频繁" 的场景。
- 典型场景 :
- 白名单/黑名单列表。
- 配置信息列表(极少更新)。
- 监听器列表(Listener List):Spring 事件机制、GUI 事件监听。
- ❌ 禁用场景 :
- 列表元素多且频繁增删(如:实时订单列表、高频日志缓冲)。这时候请用
Collections.synchronizedList或者ConcurrentLinkedQueue
- 列表元素多且频繁增删(如:实时订单列表、高频日志缓冲)。这时候请用
第三章:ConcurrentSkipListMap ------ "有序的高铁"
ConcurrentSkipListMap 是 咱们Java 中唯一一个支持高并发 且Key 有序 的 Map 实现。它是 TreeMap 的并发版本。
比喻 :这是一列高铁。
- 有序:站点(Key)严格按顺序排列(自然排序或自定义 Comparator)。
- 高速 :利用跳表 (Skip List) 结构,实现 O(logN)O(logN) 的查找、插入和删除,且无需全局锁。
- 对比 :
ConcurrentHashMap是"直升机",速度最快但不管顺序;TreeMap是"绿皮火车",有序但慢(全局锁);ConcurrentSkipListMap是"高铁",既快又有序。
底层原理:跳表 (Skip List) + CAS
1.为什么不用红黑树?
TreeMap 基于红黑树;红黑树在插入/删除时需要复杂的旋转操作来保持平衡。在并发环境下,给红黑树加细粒度锁非常困难,容易导致死锁或性能瓶颈。
2.跳表是什么?多层有序的链表
跳表是一种基于链表 的概率数据结构,通过建立多层索引来加速查找:以空间换时间
-
第 0 层:包含所有元素的标准链表
-
第 1 层:随机抽取部分元素作为索引,指向下层
-
第 N 层:索引越来越少,查找跨度越来越大
-
这保证了高层节点稀疏,低层节点密集,维持整体的平衡性
Level 2: 1 -----------------> 9 -----------------> 15 -> null
| | |
Level 1: 1 -------> 5 -------> 9 -------> 13 -------> 15 -> null
| | | | |
Level 0: 1 -> 3 -> 5 -> 7 -> 9 -> 11 -> 13 -> 15 -> null
查找过程:从最高层开始向右找,发现大于目标值就向下沉一层。就像坐高铁:先坐快车(高层索引)跨越大距离,快到站了换乘慢车(低层)精准停靠。
| 特性 | 红黑树 (Red-Black Tree) | 跳表 (Skip List) |
|---|---|---|
| 结构 | 严格的平衡二叉树 | 概率平衡的多层链表 |
| 插入/删除 | 需要复杂的旋转操作来维持平衡 | 只需修改指针,无需旋转 |
| 并发难度 | 极难。旋转操作涉及多个节点引用变更,很难拆分锁粒度,容易死锁。 | 容易。插入/删除只影响局部相邻节点,很容易通过 CAS 或 细粒度锁 实现无锁/低锁并发。 |
| 范围查询 | 需要中序遍历,稍麻烦 | 天然链表结构,范围遍历极其高效 |
| JDK 选择 | TreeMap (单线程), CHM (桶内冲突解决) |
ConcurrentSkipListMap (全并发有序 Map) |
3. 并发控制:CAS + 节点锁
-
无锁读取:读操作完全无锁,利用 volatile 保证可见性。
-
局部锁/CAS 写入 :插入/删除时,只锁定相邻的节点,而不是整个 Map。利用 CAS 操作更新指针。
-
随机层级 :新节点插入时,通过随机算法决定它出现在哪几层索引中(概率通常为 0.5)
// 简化的跳表节点结构
static final class Node<K,V> {
final K key;
volatile Object value;
volatile Node<K,V> next;
// 其他字段...Node(K key, Object value, Node<K,V> next) { this.key = key; this.value = value; this.next = next; }}
| 特性 | ConcurrentHashMap | ConcurrentSkipListMap |
|---|---|---|
| 底层结构 | 数组 + 链表/红黑树 | 跳表 (Skip List) |
| Key 顺序 | 无序 (JDK8 后部分有序但不保证) | 严格有序 (自然排序/Comparator) |
| 性能 | ⭐⭐⭐⭐⭐ (最快) | ⭐⭐⭐⭐ (略慢于 CHM,但优于同步 TreeMap) |
| Null Key/Value | 不允许 | 不允许 |
| 适用场景 | 绝大多数 KV 存储,不关心顺序 | 需要范围查询 (subMap, headMap) 或有序遍历 |
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.NavigableMap;
public class SkipListDemo {
public static void main(String[] args) {
// 创建一个有序的并发 Map
ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();
// 插入数据
for (int i = 10; i >= 1; i--) {
map.put(i, "Value-" + i);
}
// 核心优势:高效的范围查询 (Range Query)
// 获取 key 在 [5, 8] 之间的所有数据
NavigableMap<Integer, String> subMap = map.subMap(5, true, 8, true);
System.out.println("范围 [5, 8] 的数据:");
for (var entry : subMap.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
// 输出: 5->Value-5, 6->Value-6, 7->Value-7, 8->Value-8
// 注意:输出是有序的!CHM 做不到这一点。
// 查找第一个大于等于 6 的元素
var ceiling = map.ceilingEntry(6);
System.out.println(">=6 的第一个元素: " + ceiling);
// 并发安全测试
// 多个线程同时 put 和 subMap 遍历,不会报错
}
}
综合选型决策树
面对并发容器,请问自己三个问题:
-
需要有序吗?
- 是 →→
ConcurrentSkipListMap(Map) 或ConcurrentSkipListSet(Set)。 - 否 →→ 继续问。
- 是 →→
-
是生产者 - 消费者模型吗?需要阻塞等待吗?
- 是 →→
BlockingQueue系列。- 要限流/省内存? →→
ArrayBlockingQueue. - 要高吞吐? →→
LinkedBlockingQueue(记得设容量!). - 要优先级? →→
PriorityBlockingQueue. - 要延时? →→
DelayQueue.
- 要限流/省内存? →→
- 否 →→ 继续问。
- 是 →→
-
读写的比例是多少?
- 读 >> 写 (如 99% 读,1% 写),且写操作不频繁 →→
CopyOnWriteArrayList. - 读写均衡 或 写较多 →→
ConcurrentHashMap(首选) 或ConcurrentLinkedQueue(无锁队列).
- 读 >> 写 (如 99% 读,1% 写),且写操作不频繁 →→
Collections.synchronizedList (包装器模式)
本质:它是一个装饰器 (Decorator) 。它接收一个普通的 ArrayList 或 LinkedList,然后在每个方法外部包裹一把全局锁 (synchronized)
List<String> list = Collections.synchronizedList(new ArrayList<>());
// 正确用法:遍历时必须手动同步!
// 因为 iterator() 返回的迭代器不是线程安全的
synchronized(list) {
Iterator<String> it = list.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
}
// 错误用法:直接遍历,可能抛 ConcurrentModificationException
// for(String s : list) { ... }
ConcurrentLinkedQueue (无锁队列)
基于 CAS (Compare-And-Swap) 算法实现的无锁 (Lock-Free) 单向链表队列
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
queue.offer("A");
queue.offer("B");
// 遍历无需加锁!
// 它的迭代器是 Weakly Consistent (弱一致性)
// 意味着:它能保证遍历到创建迭代器时已存在的元素,但不保证看到之后的修改,也不会抛异常
for(String s : queue) {
System.out.println(s);
}
// 常用方法
String head = queue.poll(); // 获取并移除头元素,空则返回 null (非阻塞)
String peek = queue.peek(); // 获取不移除
- 适用场景 :
- 高并发生产者 - 消费者模型,且不需要阻塞等待(允许轮询)。
- 作为高性能的任务缓冲区。
- 替代
synchronizedList获得更高吞吐。
| 容器 | 比喻 | 核心特性 | 适用场景 | 避坑指南 |
|---|---|---|---|---|
| ArrayBlockingQueue | 🚇 固定车厢地铁 | 有界、数组、单锁 | 严格控内存、任务队列 | 容量设太小会阻塞生产者 |
| LinkedBlockingQueue | 🚄 无限列车 | 可选有界、链表、读写分离锁 | 高吞吐任务队列 | 默认无界,易 OOM,务必设容量 |
| CopyOnWriteArrayList | 🚌 读多写少大巴 | 写时复制、读无锁、最终一致 | 白名单、配置列表、监听器 | 严禁用于写频繁场景,否则 CPU/GC 爆炸 |
| ConcurrentSkipListMap | 🚅 有序高铁 | 跳表、有序、CAS | 范围查询、有序遍历、排行榜 | 性能略低于 CHM,不需要顺序时别用 |
| ConcurrentHashMap | 🚁 并发直升机 | 哈希、CAS+synchronized、无序 | 通用 KV 存储 | 无法进行范围查询,Key 无序 |