前言:
好久没有发文章了,其实并不是没写,而是最近在写自己的项目,没有什么时间来写,还有六七篇存稿没有整体,都是好几万字的,后续会慢慢整理,主要是关于agent的相关内容,同步进行的是面试的一些八股,算法,因此在这里给大家分享一些八股吧,主要是自己一个人看也记不住多少,自己写出来并且能讲明白印象更深一点。
今天看的主要是java并发工具还有锁相关的知识,这些都是面试的高频考点,需要重点掌握。
我们先整体的看一下:

先明确了整体的框架,我们主要抓住重点的进行学习。
实际最常用的是:线程池、ConcurrentHashMap、CountDownLatch、CompletableFuture。
synchronized 底层原理
这是面试超高频 题,核心就一句话:synchronized 的锁会根据竞争情况,从"轻"到"重"逐级升级,尽量少用重量级锁。
一、先搞懂锁存在哪:对象头
每个 Java 对象在内存里都有一段对象头(Mark Word),锁的状态就记录在这里。
64 位 JVM 下,Mark Word 主要存这些信息(不同锁状态存的不一样):
锁状态 Mark Word 内容 无锁 hashcode + 分代年龄 + 锁标志位(01) 偏向锁 线程ID + 时间戳 + 分代年龄 + 标志位(01) 轻量级锁 指向栈中锁记录的指针 + 标志位(00) 重量级锁 指向 Monitor 的指针 + 标志位(10) 关键:锁的信息不在你的业务对象上,而在对象头里。
二、四种锁状态 & 升级过程
无锁 → 偏向锁 → 轻量级锁 → 重量级锁 (只有一个线程) (少量竞争、自旋) (竞争激烈)注意:锁升级是单向的,不能降级(偏向锁撤销除外)。
三、逐个拆解
1. 偏向锁(Biased Locking)
场景:只有一个线程反复进入同步块。
原理:
第一个线程来的时候,把自己的线程 ID 记到对象头的 Mark Word 里
以后这个线程再来,检查 Mark Word 里是不是自己的 ID,是就直接进去,连 CAS 都不用
相当于这个锁偏向我,我随便进
为什么快
几乎零开销,就是一次比较。
什么时候升级
有第二个线程 来竞争时,偏向锁就要撤销,升级为轻量级锁。
补充:JDK 15 之后默认关闭并废弃了偏向锁,因为维护成本高、收益有限。但面试还是会问。
2. 轻量级锁(Lightweight Lock)
场景:多个线程竞争,但竞争不激烈,锁持有时间短。
原理(CAS + 自旋):
线程在自己的栈帧里创建一个锁记录(Lock Record)
用 CAS 把对象头的 Mark Word 替换成指向自己锁记录的指针
CAS 成功 → 拿到锁;失败 → 说明有竞争,自旋重试
自旋一定次数还拿不到 → 升级为重量级锁
为什么比重量级快
不涉及操作系统,在用户态用 CAS 自旋解决,避免线程阻塞/唤醒的开销。
自旋的代价:
自旋就是空转 CPU。如果锁一直被占用,自旋就是白费 CPU,所以不能无限自旋。
JDK 6 后引入自适应自旋:自旋次数根据上次成功情况动态调整。
3. 重量级锁(Heavyweight Lock)
场景:竞争激烈,或锁持有时间长。
原理:
依赖操作系统的 Monitor(管程,C++ 实现,存在对象头指向的 ObjectMonitor 里)
抢不到锁的线程直接阻塞(挂起),进入等待队列
涉及用户态 ↔ 内核态切换,开销大
ObjectMonitor 结构(了解):
_owner:当前持有锁的线程
_EntryList:等待锁的线程队列
_WaitSet:调用 wait() 后进入的等待队列
_count:重入次数为什么慢
线程阻塞和唤醒需要操作系统介入,上下文切换成本高。
四、锁优化补充(面试加分项)
除了锁升级,JVM 还有这些优化:
优化 说明 锁消除 逃逸分析发现锁对象不会被其他线程访问,直接去掉锁 锁粗化 连续多次加锁解锁,合并成一次大锁 自适应自旋 根据历史成功率动态决定自旋次数 锁消除例子:
java public String test() { StringBuffer sb = new StringBuffer(); // 局部变量,不会逃逸 sb.append("a"); return sb.toString(); } // StringBuffer 方法有 synchronized,但对象是局部的,JVM 直接消除锁
五、一句话总结
synchronized 通过对象头 Mark Word 记录锁状态,采用锁升级 策略:
单线程用偏向锁 (记线程ID,零开销);
少量竞争用轻量级锁 (CAS + 自旋,用户态解决);
激烈竞争用重量级锁 (操作系统 Monitor,阻塞排队)。
目的是尽量把锁控制在用户态,减少内核态切换开销 。
此外还有锁消除、锁粗化、自适应自旋等优化。
六、常见追问
Q:偏向锁为什么被废弃
A:撤销偏向锁需要 STW(Stop The World),在高并发场景下维护成本大于收益,JDK 15 后默认关闭。
Q:轻量级锁自旋多少次升级
A:不是固定值,JDK 6 后是自适应自旋,根据上次在该锁上自旋的成功率动态调整。
Q:synchronized 和 ReentrantLock 底层区别
A:synchronized 是 JVM 层面(Monitor),ReentrantLock 是 JDK 层面(基于 AQS + CAS)
ReentrantLock
是什么: JDK 提供的可重入锁,基于 AQS 实现,手动加锁解锁。
java Lock lock = new ReentrantLock(); lock.lock(); try { i++; } finally { lock.unlock(); // 必须手动释放! }比 synchronized 多的能力:
能力 方法 场景 可中断 lockInterruptibly()防止死等 可超时 tryLock(3, SECONDS)抢不到就放弃 公平锁 new ReentrantLock(true)需要按顺序 多条件 Condition精确唤醒 使用场景:
java // 场景1:尝试获取锁,超时就放弃(防止死等) if (lock.tryLock(3, TimeUnit.SECONDS)) { try { /* 干活 */ } finally { lock.unlock(); } } else { // 3秒没抢到,走别的逻辑 } // 场景2:公平锁,按排队顺序拿锁 Lock fairLock = new ReentrantLock(true);什么时候用 ReentrantLock 而不是 synchronized
需要超时、可中断、公平、多条件这些高级功能时。
ReentrantReadWriteLock ------ 读多写少
是什么: 一把锁拆成读锁 + 写锁。
核心规则:
读锁 + 读锁 → 可以共存(多个线程同时读) 读锁 + 写锁 → 互斥 写锁 + 写锁 → 互斥为什么需要
普通锁:读读也互斥,太浪费。但读操作不会改数据,多个线程一起读完全没问题。
java ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); Lock readLock = rwLock.readLock(); Lock writeLock = rwLock.writeLock(); // 读操作:多个线程可同时进行 readLock.lock(); try { return map.get(key); } finally { readLock.unlock(); } // 写操作:独占 writeLock.lock(); try { map.put(key, value); } finally { writeLock.unlock(); }使用场景: 读多写少,比如本地缓存。
text
缓存:读 100 次,写 1 次 → 用读写锁,100 个读可以并行注意: 如果读写差不多,甚至写多,读写锁反而更慢(维护两把锁有开销)。
StampedLock ------ 读写锁的升级版(JDK8)
是什么: 比读写锁更快的锁,支持乐观读。
java StampedLock lock = new StampedLock(); // 乐观读(不加锁,读完检查有没有被改) long stamp = lock.tryOptimisticRead(); int data = this.data; if (!lock.validate(stamp)) { // 读期间被改了 // 升级为悲观读 stamp = lock.readLock(); try { data = this.data; } finally { lock.unlockRead(stamp); } }使用场景: 读极多、写极少,追求极致性能。实际项目用得很少,面试会问。
CAS / 原子类 ------ 无锁
是什么: 不加锁,靠 CPU 的 CAS 指令保证原子性。
java AtomicInteger i = new AtomicInteger(0); i.incrementAndGet(); // 无锁自增,比 synchronized 快使用场景:
简单计数(访问量、库存)
状态标记 (
AtomicBoolean)无锁数据结构 (
ConcurrentHashMap底层)java
// 典型:库存扣减
AtomicInteger stock = new AtomicInteger(100);
if (stock.decrementAndGet() >= 0) {
// 扣减成功
}注意: 高并发下 CAS 自旋会浪费 CPU,此时用
LongAdder更好。
ConcurrentHashMap
一句话定位
线程安全的 HashMap。 多线程环境下替代
HashMap,性能又远好于Hashtable。
为什么不用 HashMap / Hashtable
集合 线程安全 性能 问题 HashMap 不安全 高 并发 put 会丢数据、死循环(JDK7)、数据错乱 Hashtable 安全 低 所有方法加 synchronized,全表一把锁,并发差ConcurrentHashMap 安全 高 锁粒度细,并发好 Hashtable 的问题: 一个线程 put,其他所有线程连 get 都被阻塞。太粗暴。
ConcurrentHashMap 怎么做到又安全又快
JDK 7:分段锁(Segment)
把整个表分成 16 个 Segment,每个 Segment 一把锁。
ConcurrentHashMap ├── Segment[0] ← 锁A ├── Segment[1] ← 锁B ├── ... └── Segment[15] ← 锁P不同 Segment 可以并发写,锁冲突概率降到 1/16。
JDK 8:CAS + synchronized(锁桶头节点)
放弃分段锁,改成:
数组 + 链表 + 红黑树(和 HashMap 一样)
锁的粒度是单个桶(数组的某个下标)
桶为空时用 CAS 放第一个节点(无锁)
桶非空时用 synchronized 锁住桶的头节点
text
数组 table [0] → null [1] → A → B → C ← 锁这个头节点 A [2] → null [3] → D → E ← 锁这个头节点 D [4] → 红黑树...为什么快
不同桶可以真正并行写
空桶用 CAS,完全无锁
只有哈希冲突到同一个桶,才用 synchronized,且只锁那一个桶
4. 核心操作原理
put 流程(JDK 8):
text
1. 计算 key 的 hash,定位到数组下标 i 2. 如果 table[i] == null: → CAS 直接放入新节点(无锁,成功就完事) 3. 如果 table[i] != null: → synchronized 锁住 table[i] 这个头节点 → 遍历链表/红黑树,找到就覆盖,找不到就尾插 4. 链表长度 ≥ 8 且数组长度 ≥ 64 → 转红黑树 5. 元素个数 +1,用 CAS 更新 size(baseCount + CounterCell 分段计数)get 流程:
text
1. 算 hash 定位下标 2. 遍历链表/红黑树找 key 3. 全程不加锁(节点 value 和 next 用 volatile 保证可见性)get 为什么不用加锁
因为节点的
val和next是volatile的,改了别的线程立刻能看到,不会读到脏数据常见追问
Q:ConcurrentHashMap 允许 null 键值吗
A:不允许。因为 get 返回 null 时无法区分"key 不存在"还是"value 就是 null",并发下有二义性。
Q:size() 是精确的吗
A:JDK 8 用 baseCount + CounterCell 累加,并发下是近似值,不是强一致。
Q:扩容时其他线程能操作吗
A:能。扩容是多线程协助迁移,其他线程 put 时如果发现正在扩容,会帮忙搬一部分数据。
补充
ConcurrentHashMap的扩容机制:
设计 目的 多线程协助 避免一个线程搬、其他人干等 CAS 抢任务 无锁分配搬迁任务,高效 ForwardingNode 标记搬完的桶,转发请求 锁桶头 保证正在搬的桶不出错 sizeCtl 负数 标识扩容状态,让其他线程感知 目标:扩容期间,读写操作尽量不被长时间阻塞。
JDK 8 采用多线程协助扩容:
触发扩容时,把 sizeCtl 设为负数标识扩容中,创建新数组 nextTable;
每个线程用 CAS 抢 transferIndex,领取一段桶进行搬迁;
搬某个桶时,用 synchronized 锁住桶头节点,把元素重新 hash 到新数组,搬完在旧桶放 ForwardingNode 标记;
其他线程 put/get 遇到 ForwardingNode,会转发到新数组操作;
其他线程 put 时发现正在扩容,也会主动来帮忙搬。
相比 JDK 7 的单线程扩容,JDK 8 多线程协助,扩容期间尽量不阻塞读写。
CountDownLatch
1. 作用
一个线程等 N 个线程完成。
2. 原理
基于 AQS 的共享锁 ,state 初始化为 N,countDown() 就是 state-1,await() 就是等 state=0。
3. 代码
java
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
// 干活
} finally {
latch.countDown();
}
}).start();
}
latch.await(); // 主线程阻塞,直到计数为 0
System.out.println("全部完成");
4. 使用场景
-
主线程等所有子任务完成(并发汇总)
-
模拟并发测试(让多个线程同时开始)
-
服务启动等待组件初始化
5. 面试追问
Q:CountDownLatch 能重用吗
不能,计数到 0 就废了。要重用用 CyclicBarrier。
Q:await() 能超时吗
能,await(3, TimeUnit.SECONDS),返回 false 表示超时。
Q:底层原理
AQS 共享模式,state 为计数,countDown 用 CAS 递减,减到 0 唤醒所有等待线程。
AQS 是什么
AQS 是面试锁 话题的终极考点。ReentrantLock、CountDownLatch、Semaphore、读写锁全都基于它。搞懂它,前面所有锁的底层就串起来了。
一、一句话定义
AQS = AbstractQueuedSynchronizer(抽象队列同步器),是 JDK 提供的一个"构建锁和同步器的框架"。
它把"抢锁、排队、阻塞、唤醒"这套通用逻辑都写好了,你只需要告诉它"能不能拿到锁"。
打个比方:
AQS 就像一栋毛坯房:
框架、水电、墙体(排队、阻塞、唤醒)它都建好了
你只需要装修(实现"能不能拿锁"的逻辑)
ReentrantLock、CountDownLatch、Semaphore 都是基于这套毛坯房装修出来的成品。
二、AQS 的核心:两个东西
AQS 内部就靠两样东西运转:
java
public abstract class AbstractQueuedSynchronizer { // 1. 同步状态(核心!) private volatile int state; // 2. 等待队列(双向链表) private transient volatile Node head; private transient volatile Node tail; }核心1:state(同步状态)
state 是一个 int,不同子类给它不同含义:
子类 state 表示什么 ReentrantLock 锁被重入几次(0=没锁,1=拿1次,2=重入) CountDownLatch 还剩几个任务没完成 Semaphore 还剩几个许可证 state 的修改用 CAS 保证原子性。
核心2:等待队列(CLH 变体的双向链表)
抢不到锁的线程,包装成 Node,排进这个队列。
text
head tail │ │ ▼ ▼ [Node] ⇄ [Node] ⇄ [Node] ⇄ [Node] 线程B 线程C 线程D 线程E (正在等)(等待中)(等待中)(等待中)
每个 Node 装一个等待的线程
前驱释放锁后,唤醒后继
双向链表方便节点取消、找前驱
三、AQS 的精髓:模板方法模式
AQS 把"抢锁"分成两部分:
text
┌─────────────────────────────────────┐ │ AQS 负责的(通用逻辑,写好了) │ │ ├── 维护 state │ │ ├── 维护等待队列 │ │ ├── 线程排队、阻塞(park) │ │ └── 唤醒后继(unpark) │ ├─────────────────────────────────────┤ │ 子类负责的(业务逻辑,你来写) │ │ └── tryAcquire:能不能拿到锁? │ └─────────────────────────────────────┘AQS 定义了几个"留给子类实现"的方法:
java
// 独占模式:能不能获取 protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); // 空实现,子类必须重写 } // 独占模式:能不能释放 protected boolean tryRelease(int arg) { ... } // 共享模式:能不能获取 protected int tryAcquireShared(int arg) { ... } // 共享模式:能不能释放 protected boolean tryReleaseShared(int arg) { ... }你只需要重写这几个方法,队列、阻塞、唤醒 AQS 全帮你搞定。
四、两种模式:独占 vs 共享
模式 含义 实现 独占模式(Exclusive) 同一时刻只有一个线程能拿到 ReentrantLock 共享模式(Shared) 多个线程可以同时拿到 CountDownLatch、Semaphore、读写锁的读锁 独占 :像厕所单间,一个人进去锁门,别人等。
共享:像公园长椅,可以坐好几个人,坐满了才需要等。
五、加锁流程(独占模式,重点)
以
ReentrantLock.lock()为例:text
lock() │ ▼ AQS.acquire(1) │ ▼ ① tryAcquire(1) —— 子类实现,尝试拿锁 │ ├── 成功 → 拿到锁,直接返回,干活 │ └── 失败 ↓ ▼ ② addWaiter() —— 把当前线程包装成 Node,加入队列尾部 │ ▼ ③ acquireQueued() —— 在队列里"自旋 + 阻塞" │ ├── 如果自己是队首(head.next),再试 tryAcquire │ 成功 → 出队,拿到锁 │ 失败 → 继续 │ └── 不是队首,或试失败 → LockSupport.park() 阻塞 │ ▼ ④ 前驱节点释放锁时,unpark 唤醒自己 │ ▼ 回到 ③ 再试简化记忆:
text
尝试拿锁 → 失败入队 → 自旋/阻塞 → 被唤醒 → 再尝试 → 成功出队
六、ReentrantLock 怎么用 AQS 实现
加锁
java
// ReentrantLock 的 tryAcquire(非公平版,简化) protected boolean tryAcquire(int acquires) { int c = getState(); if (c == 0) { // 没人拿锁,CAS 抢 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } } else if (current == getExclusiveOwnerThread()) { // 是当前线程拿的 → 可重入,state+1 setState(c + acquires); return true; } return false; // 抢不到 }看,ReentrantLock 只做了两件事:
state 为 0 → CAS 抢
state 不为 0 且是自己 → state+1(重入)
剩下的排队、阻塞、唤醒全是 AQS 干的。
解锁
java
protected boolean tryRelease(int releases) { int c = getState() - releases; // state - 1 if (c == 0) { setExclusiveOwnerThread(null); // 真正释放 setState(c); return true; // 返回 true 才会唤醒后继 } setState(c); // 还没释放完(重入),返回 false return false; }只有 state 减到 0 才算真正释放,才唤醒队列里的下一个。
七、CountDownLatch 怎么用 AQS(共享模式)
java
// 初始化,state = 3 new CountDownLatch(3); // countDown:state - 1 countDown(); → tryReleaseShared(1) → CAS 把 state 减 1 → state 减到 0 → 返回 true → 唤醒所有等待线程 // await:等 state = 0 await(); → tryAcquireShared(1) → state == 0 ? 返回 1(成功): 返回 -1(失败,入队阻塞)state 就是"还剩几个任务"。减到 0,所有人放行。
八、公平锁 vs 非公平锁(AQS 里怎么体现)
区别就在
tryAcquire里加不加"检查队列"这一步。非公平锁(默认)
java
protected boolean tryAcquire(int acquires) { // 直接 CAS 抢!不管队列里有没有人排队 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } // ... }新来的线程可以直接插队抢锁。
公平锁
java
protected boolean tryAcquire(int acquires) { // 先检查:队列里有没有人排队? if (!hasQueuedPredecessors() && // ← 关键:有前驱就不抢 compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } // ... }公平锁多了一句
hasQueuedPredecessors():如果前面有人在等,我就乖乖排队,不抢。
公平锁 非公平锁 抢锁前 检查队列有没有前驱 直接 CAS 抢 性能 慢(上下文切换多) 快(减少唤醒) 饥饿 不会 可能 为什么默认非公平? 因为非公平可以减少线程唤醒,吞吐量更高。
九、AQS 里几个关键方法(面试可能问)
方法 作用 acquire(int)独占获取(含入队阻塞) acquireShared(int)共享获取 release(int)独占释放 releaseShared(int)共享释放 hasQueuedPredecessors()队列里有没有前驱(公平锁用) LockSupport.park()阻塞当前线程 LockSupport.unpark(t)唤醒指定线程
park/unpark和wait/notify的区别(常问):
park/unpark wait/notify 是否需要持有锁 不需要 必须在 synchronized 里 唤醒精度 可以精确唤醒指定线程 notify 随机唤醒一个 顺序 unpark 可以先于 park notify 先于 wait 会丢失
AQS 是什么
AQS 是 AbstractQueuedSynchronizer,是 JUC 里构建锁和同步器的基础框架。ReentrantLock、CountDownLatch、Semaphore 都基于它。
核心是一个 volatile 的 state 变量 + 一个双向等待队列:
state 表示同步状态,不同子类含义不同(ReentrantLock 里是重入次数,CountDownLatch 里是剩余计数)
抢不到锁的线程包装成 Node,进入队列排队,用 LockSupport.park 阻塞
它用了模板方法模式 :AQS 负责队列、阻塞、唤醒这些通用逻辑,子类只需实现
tryAcquire/tryRelease等方法,告诉 AQS"能不能拿到锁"。AQS 支持两种模式:独占 (ReentrantLock)和共享 (CountDownLatch、Semaphore)。公平锁和非公平锁的区别,就在于
tryAcquire里是否检查队列前驱。AQS 里的 Condition(条件队列)
Condition 是 AQS 里除了等待队列之外的第二个队列 ,用来实现
await/signal,对应synchronized里的wait/notify。
一、先搞懂 Condition 是什么
Condition = 条件变量,让线程在某个条件下等待,条件满足时被唤醒。
类比:
synchronized+wait/notify:只有一个"休息室",所有等的线程都挤在一起
ReentrantLock+Condition:可以开多个休息室,不同条件去不同休息室等java
// synchronized 版本 synchronized (lock) { while (条件不满足) lock.wait(); // 干活 lock.notifyAll(); }java
// Condition 版本(更灵活) lock.lock(); try { while (条件不满足) condition.await(); // 干活 condition.signal(); } finally { lock.unlock(); }核心区别:一个 Lock 可以创建多个 Condition,精确唤醒。
二、为什么需要 Condition(对比 wait/notify)
问题:wait/notify 只能有一个等待队列
经典例子:阻塞队列(生产者-消费者)
java
// 用 synchronized + wait/notify 的问题 synchronized (lock) { // 生产者:队列满了要等 while (queue.size() == MAX) lock.wait(); // 消费者:队列空了要等 while (queue.size() == 0) lock.wait(); lock.notifyAll(); // 只能全部唤醒,分不清唤醒谁 }问题: 生产者和消费者在同一个等待队列里。生产者唤醒时,可能唤醒的也是生产者,白唤醒,还得继续等。
解决:用两个 Condition 分开等
java
// 用 Condition,生产者、消费者各一个队列 private final Condition notFull = lock.newCondition(); // 生产者在这等 private final Condition notEmpty = lock.newCondition(); // 消费者在这等 // 生产者 lock.lock(); try { while (queue.size() == MAX) notFull.await(); // 满了,在 notFull 等 queue.add(e); notEmpty.signal(); // 精确唤醒消费者 } finally { lock.unlock(); } // 消费者 lock.lock(); try { while (queue.size() == 0) notEmpty.await(); // 空了,在 notEmpty 等 queue.poll(); notFull.signal(); // 精确唤醒生产者 } finally { lock.unlock(); }这就是 ArrayBlockingQueue 的实现原理!
三、Condition 的核心方法
方法 作用 对应 wait/notify await()释放锁,进入等待,被唤醒后重新拿锁 wait()await(time, unit)带超时的等待 wait(timeout)signal()唤醒一个等待线程 notify()signalAll()唤醒所有等待线程 notifyAll()注意:调用这些方法前必须持有锁!
java
lock.lock(); // 必须先拿锁 try { condition.await(); // 才能 await } finally { lock.unlock(); }和
wait/notify必须在 synchronized 里一样。
四、await/signal 的完整流程
await() 干了什么
text
线程A 调用 condition.await() │ ▼ ① 释放锁(state 归 0,唤醒等待队列的下一个) │ ▼ ② 把自己包装成 Node,加入 Condition 的【条件队列】 │ ▼ ③ 阻塞(park),等待被 signal │ ▼ ④ 被 signal 后,从条件队列移到【等待队列】 │ ▼ ⑤ 重新竞争锁(acquire) │ ▼ ⑥ 拿到锁,await 返回,继续执行关键点:
await 会释放锁(不然别人没法 signal)
被唤醒后要重新抢锁(所以不是立刻返回)
signal() 干了什么
text
线程B 调用 condition.signal() │ ▼ ① 从条件队列取出第一个 Node │ ▼ ② 把它移到等待队列(不是直接唤醒!) │ ▼ ③ 等它前面的线程释放锁后,才会被唤醒去抢锁关键:signal 只是"搬家",从条件队列搬到等待队列,真正唤醒要等锁释放。
五、两个队列的关系(核心图)
text
AQS 内部有两个队列: ① 等待队列(同步队列) ┌──────────────────────────────────┐ head ⇄ Node ⇄ Node ⇄ Node ⇄ tail (抢锁失败的线程在这排队) └──────────────────────────────────┘ ② 条件队列(Condition Queue) ┌──────────────────────────────────┐ firstNode → Node → Node → lastNode (调用 await 的线程在这等) └──────────────────────────────────┘await 和 signal 就是在这两个队列之间"搬家":
text
调用 await: 线程从【运行】→ 释放锁 → 进入【条件队列】 调用 signal: 线程从【条件队列】→ 移到【等待队列】→ 等锁 → 拿到锁 → 继续运行画个流程:
text
线程运行 │ │ await() ← 释放锁,进条件队列 ▼ 条件队列(等待 signal) │ │ signal() ← 被移到等待队列 ▼ 等待队列(重新抢锁) │ │ 抢到锁 ▼ 线程继续运行
六、完整代码示例:手写阻塞队列
java
public class MyBlockingQueue<T> { private final Object[] items; private int count, putIndex, takeIndex; private final ReentrantLock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); // 生产者等 private final Condition notEmpty = lock.newCondition(); // 消费者等 public MyBlockingQueue(int capacity) { items = new Object[capacity]; } // 生产者 public void put(T x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); // 满了,生产者在这等 items[putIndex] = x; if (++putIndex == items.length) putIndex = 0; count++; notEmpty.signal(); // 唤醒消费者 } finally { lock.unlock(); } } // 消费者 public T take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); // 空了,消费者在这等 T x = (T) items[takeIndex]; if (++takeIndex == items.length) takeIndex = 0; count--; notFull.signal(); // 唤醒生产者 return x; } finally { lock.unlock(); } } }这就是 ArrayBlockingQueue 的核心逻辑。
七、面试必问的几个点
Q1:await 和 wait 的区别
await wait 所属 Condition Object 必须持有 Lock synchronized 队列 可以有多个 Condition 只有一个 唤醒 signal 精确唤醒 notify 随机 Q2:为什么 await 必须重新抢锁?
因为
await会释放锁,别的线程可能在此期间改了状态。被唤醒后必须重新拿锁,重新检查条件 ,所以要用while而不是if:java
while (条件不满足) condition.await(); // 必须 while,不能 if用 if 的坑: 被唤醒后不重新检查,可能条件已经又不满足了(虚假唤醒)。
Q3:什么是虚假唤醒?
线程可能在没有 signal 的情况下被唤醒(操作系统层面)。所以必须用
while循环重新检查条件。Q4:signal 和 signalAll 区别
signal:唤醒条件队列第一个
signalAll:唤醒所有一般用 signal 更高效,但如果多个线程等不同条件,得用 signalAll。
Q5:Condition 队列和等待队列的区别
等待队列 条件队列 触发 抢锁失败 调用 await 目的 等锁 等条件 出队 拿到锁 被 signal 结构 双向链表 单向链表
八、一句话总结
Condition 是 AQS 的条件队列,配合 Lock 实现 await/signal,对应 wait/notify。
一个 Lock 可以有多个 Condition,实现精确唤醒(生产者、消费者各一个)
await 流程:释放锁 → 进条件队列 → 阻塞 → 被 signal 移到等待队列 → 重新抢锁 → 继续
两个队列:等待队列(等锁)、条件队列(等条件),signal 就是"搬家"
必须用 while 检查条件,防止虚假唤醒