⚖️ AQS 与 JUC
AQS 原理 · ReentrantLock 公平/非公平 · CountDownLatch/CyclicBarrier/Semaphore · 原子类 · 并发容器
1. AQS 的核心原理?
AQS(AbstractQueuedSynchronizer):JUC 锁和同步器的"地基"。核心三要素:
- volatile int state:同步状态(ReentrantLock 中 0=无锁,≥1 为重入次数;Semaphore 中为剩余许可数)
- CLH 变体等待队列:抢锁失败的线程封装成 Node 入队,前驱节点释放时通过 LockSupport.unpark 唤醒后继(基于链表的前驱唤醒,避免惊群)
- 模板方法模式:tryAcquire/tryRelease 等钩子方法由子类实现(各自定义 state 语义),AQS 提供 acquire/release 骨架流程
加锁流程:tryAcquire 尝试 CAS 改 state → 成功则持有;失败则入队挂起(park)→ 前驱释放时 unpark 唤醒 → 重新竞争。
AQS 家族一览
ReentrantLock // 可重入互斥锁(state = 重入次数)
Semaphore // 信号量(state = 许可数)
CountDownLatch // 倒计时门闩(state = 计数)
ReentrantReadWriteLock // 读写锁(state 高16位读锁、低16位写锁)
ThreadPoolExecutor // Worker 也用 AQS 实现不可重入互斥
ArrayBlockingQueue // 条件队列 Conditon 基于 AQS
🎯 面试要点
- 面试常问:手撕 AQS 简化版——答出"state + 双向队列 + CAS + park/unpark"就过关
- CLH 队列:自旋(前驱节点轮询)在 Java 中改造为阻塞(park),叫"CLH 变体"
- 为什么用前驱唤醒:每个节点只被自己的前驱唤醒,避免 notifyAll 式惊群
2. ReentrantLock 与 synchronized 的区别?公平锁和非公平锁?
区别:
- synchronized:JVM 内置(monitor),自动加解锁,支持锁升级;不可中断;非公平
- ReentrantLock:JUC 类,需手动 lock/unlock(必须 finally 中 unlock);可中断(lockInterruptibly)、可超时(tryLock(timeout))、可公平(构造参数)、可绑定多个 Condition
公平/非公平实现:
- 非公平:新线程直接
CAS 抢锁,抢不到再排队 → 吞吐高,但可能"插队"饿死队尾线程 - 公平:
hasQueuedPredecessors()检查队列是否有等待者,有则乖乖排队 → 无饥饿,吞吐略低
ReentrantLock + Condition 实现精准唤醒
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 生产者
lock.lock();
try {
while (queue.isFull()) notFull.await(); // 等"不满"
queue.add(x);
notEmpty.signal(); // 只唤醒消费者
} finally { lock.unlock(); }
// 消费者:notEmpty.await() / notFull.signal()——比 wait/notifyAll 精准,不会无效唤醒
🎯 面试要点
- 一个 lock 可 new 多个 Condition(队列场景生产/消费分开唤醒)
- 默认非公平:性能优先;业务上绝大多数场景非公平就够
- LockSupport.park/unpark:与线程许可机制,unpark 可以先于 park(无超时风险)
3. CountDownLatch / CyclicBarrier / Semaphore 的区别?
| 工具 | 场景 | 复用性 | 关键方法 |
|---|---|---|---|
| CountDownLatch | 主线程等 N 个子任务完成("等门闩放下") | 一次性,计数归 0 不可复用 | countDown() / await() |
| CyclicBarrier | N 个线程互相等齐后同时出发("凑人头") | 可循环使用(reset) | await() 凑齐 N 个放行 |
| Semaphore | 控制并发量(限流:最多 N 个同时访问) | 可反复 acquire/release | acquire() / release() |
Semaphore 限流示例
// 最多 3 个线程同时访问数据库连接
Semaphore sem = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
sem.acquire(); // 拿许可,没有则阻塞
// 访问受限资源...
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
sem.release(); // 归还许可
}
}).start();
}
🎯 面试要点
- CountDownLatch:await 是主线程,countDown 是子线程(一等多)
- CyclicBarrier:所有线程都 await,凑齐 N 个一起放行,可带 barrierAction(凑齐后执行的任务)
- Semaphore 的 fair 参数同 ReentrantLock;释放必须是 finally
4. CAS 是什么?原子类怎么工作的?ABA 问题?
CAS(Compare And Swap):硬件级原子指令。`内存值 V、期望值 A、新值 B` —— 仅当 V == A 时把 V 更新为 B,否则失败重试。JUC 通过 Unsafe.compareAndSwapInt 调用,无锁实现原子更新。
原子类(java.util.concurrent.atomic):AtomicInteger 等,用 volatile 字段 + CAS。适合高并发计数器(比加锁吞吐高)。
ABA 问题:线程 1 读 A,线程 2 改 A→B→A,线程 1 CAS 时发现还是 A,误认为没被改过。解决:AtomicStampedReference(版本号)。实际业务中 ABA 大多无害(如栈场景可能有问题)。
CAS 自旋的朴素实现
public final int incrementAndGet() {
int current;
do {
current = get(); // 读当前值
} while (!compareAndSet(current, current + 1)); // 失败就重试
return current + 1;
}
// 自旋的代价:竞争激烈时空转浪费 CPU(可配合 Thread.yield 或退避)
// JDK8+ LongAdder 优化:分 Cell 计数,减少 CAS 竞争,适合高频统计
🎯 面试要点
- CAS 缺点:自旋 CPU 开销、ABA、只能保证单个变量的原子性(多变量用锁)
- LongAdder 热点打散思路在并发计数场景优于 AtomicLong
- ConcurrentHashMap 的 put 空桶插入用的就是 CAS
5. JUC 并发容器有哪些?各解决什么问题?
- ConcurrentHashMap:线程安全 Map(详见集合框架页)
- CopyOnWriteArrayList:写时复制——写操作复制整个底层数组,读不加锁。适合"读多写极少"(如监听器列表)。缺点:写代价大、数据最终一致
- BlockingQueue 家族:ArrayBlockingQueue(有界,锁)、LinkedBlockingQueue(无界/有界,双锁)、PriorityBlockingQueue、SynchronousQueue(无缓冲,直接交接)——线程池的底层队列
- ConcurrentLinkedQueue:CAS 无锁队列,高性能但无界
- ConcurrentSkipListMap:跳表实现的有序 Map(替代 TreeMap 的并发版,支持范围查找)
🎯 面试要点
- CopyOnWriteArrayList 迭代器是弱一致:迭代中修改不抛 ConcurrentModificationException
- SynchronousQueue 常用于 Executors.newCachedThreadPool 的队列
- 阻塞队列的核心:put 满则等、take 空则等(内部用 Condition)