java多线程与juc并发编程,深入底层,搞定java面试,升级加薪,Java多线程JUC并发编程-----夏の哉---97it.---top/---14104/
Java 并发包(JUC)详解:Lock、Condition 与 AQS 底层原理
在 Java 并发编程中,synchronized关键字曾是实现线程同步的主要方式,但它存在灵活性不足(无法中断等待、不能超时获取锁)、功能有限(仅支持单一条件队列)等问题。Java 并发包(JUC)引入的Lock接口及其实现(如ReentrantLock),配合Condition和底层的 AQS(AbstractQueuedSynchronizer),构建了更强大的同步机制。理解这三者的底层原理,是掌握 Java 高级并发编程的核心。
一、AQS:同步器的 “基础设施”
AQS 是 JUC 中大多数同步工具的底层骨架,包括Lock、Semaphore、CountDownLatch等。它通过 “状态管理 + 队列调度” 实现线程的同步与协作,核心思想是 “基于状态的排队机制”。
1. 核心结构:状态与双向队列
AQS 的内部维护两个核心要素:
- 同步状态(state) :一个volatile int变量,用于表示资源的占用情况(如state=0表示锁空闲,state=1表示被占用,state>1表示重入次数)。
- 双向同步队列(CLH 队列) :当线程获取资源失败时,会被包装成节点(Node) 加入队列,等待被唤醒。节点包含线程引用、等待状态(如CANCELLED、SIGNAL)、前驱 / 后继指针。
CLH 队列是一种 “虚拟双向队列”(节点间通过指针关联,无实际容器存储),结构如下:
head(头节点,哨兵) ←→ Node1(线程A) ←→ Node2(线程B) ←→ ... ←→ tail(尾节点)
- 头节点是 “哨兵节点”(不关联线程),表示当前持有锁的线程;
- 新节点通过compareAndSetTail原子操作加入队尾,保证线程安全。
2. 核心操作:获取与释放资源
AQS 通过模板方法模式定义了获取和释放资源的流程,子类需重写tryAcquire(独占式获取)、tryRelease(独占式释放)等方法。
独占式获取资源(如ReentrantLock的lock()):
- 调用tryAcquire(arg):子类实现具体的获取逻辑(如state是否为 0,是则 CAS 修改为 1);
- 若成功,当前线程直接返回;
- 若失败,将线程包装为 Node 加入 CLH 队列,调用LockSupport.park()阻塞线程;
- 当持有资源的线程释放后,会唤醒队列中的后继节点,重复步骤 1。
独占式释放资源(如ReentrantLock的unlock()):
- 调用tryRelease(arg):子类实现释放逻辑(如state减 1,直至 0 表示完全释放);
- 若释放后状态允许(如state=0),唤醒 CLH 队列中的首个等待节点(通过LockSupport.unpark())。
3. 核心设计:模板方法与钩子函数
AQS 定义了一系列模板方法(如acquire、release),封装了队列管理、线程阻塞 / 唤醒等通用逻辑,而将具体的资源获取 / 释放逻辑委托给子类(通过重写钩子函数):
- 必须重写的方法:tryAcquire、tryRelease(独占式),tryAcquireShared、tryReleaseShared(共享式);
- 可选重写的方法:isHeldExclusively(判断是否独占持有)。
这种设计让 AQS 能适配不同的同步场景(如独占锁、共享锁),同时避免代码重复。
二、Lock 接口:比 synchronized 更灵活的锁
Lock接口是 JUC 对 “锁” 的抽象,定义了获取锁、释放锁、中断等待等操作,弥补了synchronized的不足。其核心实现类ReentrantLock(可重入锁)就是基于 AQS 实现的。
1. Lock 接口的核心方法
public interface Lock {
void lock(); // 获取锁,若失败则阻塞
void lockInterruptibly() throws InterruptedException; // 可被中断的获取
boolean tryLock(); // 尝试获取锁,立即返回结果(非阻塞)
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 超时获取
void unlock(); // 释放锁
Condition newCondition(); // 创建条件对象
}
与synchronized相比,Lock的优势在于:
- 可中断:lockInterruptibly()允许线程在等待时响应中断(如Thread.interrupt());
- 超时机制:tryLock(time)避免线程无限等待;
- 多条件队列:通过newCondition()创建多个Condition,实现更精细的线程协作。
2. ReentrantLock 的实现:基于 AQS 的独占锁
ReentrantLock通过内部类Sync(继承 AQS)实现锁逻辑,Sync有两个子类:
- NonfairSync(非公平锁,默认):线程获取锁时直接尝试抢占,可能插队;
- FairSync(公平锁):线程严格按 CLH 队列顺序获取锁,不允许插队。
非公平锁的lock()流程:
- 调用compareAndSetState(0, 1):尝试直接抢占锁(state从 0→1);
- 若成功,设置当前线程为持有者(setExclusiveOwnerThread(Thread.currentThread()));
- 若失败,调用acquire(1)进入 AQS 的获取流程:
-
- tryAcquire(1):检查state,若为 0 则再次尝试 CAS;若当前线程是持有者,则state+1(重入);
-
- 若仍失败,线程入队并阻塞。
释放锁的unlock()流程:
- 调用release(1),最终执行tryRelease(1):
-
- state减 1,若减为 0,清空持有者线程;
-
- 返回true表示完全释放,唤醒队列中的线程。
3. 可重入性的实现
ReentrantLock的 “可重入” 特性(同一线程可多次获取锁)通过state计数实现:
- 首次获取:state从 0→1;
- 再次获取(同一线程):state+1(如state=2);
- 释放时:每次unlock()对应state-1,直至state=0才完全释放锁。
三、Condition:基于 AQS 的条件队列
Condition是Lock的 “配套工具”,用于实现线程间的条件等待(类似synchronized中的wait()/notify()),但支持多个独立的条件队列(synchronized仅一个)。
1. Condition 与 Object 的等待 / 通知对比
| 特性 | Condition | Object的wait()/notify() |
|---|---|---|
| 关联锁 | 与Lock绑定 | 与synchronized绑定 |
| 条件队列数量 | 多个(每个Condition对应一个队列) | 1 个(与锁对象关联) |
| 唤醒方式 | signal()(唤醒一个)、signalAll()(唤醒全部) | notify()、notifyAll() |
| 中断响应 | 支持(awaitInterruptibly()) | 支持(被中断时抛出InterruptedException) |
| 超时等待 | 支持(await(time)) | 支持(wait(time)) |
2. Condition 的底层实现:条件队列与 AQS 的协作
Condition的实现类是ConditionObject(ReentrantLock的内部类),其底层依赖 AQS 的条件队列和锁状态。
核心结构:
- 条件队列:一个单向链表,存储调用await()的线程(节点类型与 AQS 的 Node 相同);
- 锁关联:Condition必须与Lock绑定,调用await()前必须持有锁。
await()流程(线程等待):
- 检查当前线程是否持有锁,若未持有则抛出IllegalMonitorStateException;
- 将当前线程包装为 Node 加入条件队列;
- 释放锁(调用release(state),让其他线程有机会获取锁);
- 阻塞当前线程(LockSupport.park()),等待被signal()唤醒;
- 唤醒后,重新竞争获取锁(调用acquire(state));
- 若成功获取,从await()返回;若失败,继续阻塞。
signal()流程(唤醒线程):
- 检查当前线程是否持有锁;
- 取出条件队列的首节点,将其转移到 AQS 的同步队列(CLH 队列);
- 唤醒该节点关联的线程(LockSupport.unpark()),使其有机会竞争锁。
3. 与 synchronized 的 wait/notify 对比优势
- 多条件队列:例如,一个线程池的Lock可创建两个Condition(notEmpty和notFull),分别管理 “队列非空” 和 “队列未满” 的等待线程,避免synchronized中单一队列的 “唤醒风暴”(notifyAll()唤醒所有无关线程);
- 精准控制:Condition的signal()仅唤醒一个线程,signalAll()唤醒所有,而synchronized的notify()随机唤醒一个,难以精准控制。
四、AQS、Lock、Condition 的协同关系
三者的关系可概括为:AQS 是底层骨架,Lock 是 AQS 的独占式实现,Condition 是基于 AQS 的条件等待机制。
1. 层级关系
- 底层:AQS 提供状态管理、队列调度、线程阻塞 / 唤醒等基础能力;
- 中层:Lock(如ReentrantLock)通过继承 AQS 并重写tryAcquire/tryRelease,实现独占锁的语义;
- 上层:Condition依赖Lock的 AQS 实例,利用其条件队列和同步队列,实现线程的条件等待与唤醒。
2. 协作流程(以生产者 - 消费者模型为例)
- 生产者获取Lock(lock()),若缓冲区满,调用notFull.await()进入条件队列;
- 消费者获取Lock,取出数据后调用notFull.signal(),唤醒生产者线程;
- 生产者线程从条件队列转移到同步队列,竞争锁;
- 成功获取锁后,生产者写入数据,调用notEmpty.signal()唤醒消费者,最后释放锁(unlock())。
这一过程中:
- AQS 的同步队列管理锁的竞争;
- Condition的条件队列管理线程的条件等待;
- Lock的获取与释放贯穿全程,确保操作的原子性。
五、实战与面试要点
1. 常见错误用法
- 未持有锁时调用 Condition 方法:await()/signal()必须在lock()和unlock()之间调用,否则抛出IllegalMonitorStateException;
- 忘记释放锁:lock()后未在finally中调用unlock(),可能导致死锁;
- 混淆公平锁与非公平锁:非公平锁性能更好(减少线程切换),但可能导致线程饥饿;公平锁保证顺序,但性能略低。
2. 面试高频问题
- AQS 的 state 变量为什么用 volatile?
保证state的可见性(一个线程修改后,其他线程能立即看到),配合 CAS 操作(compareAndSetState)实现原子性,确保同步状态的线程安全。
- ReentrantLock 与 synchronized 的区别?
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 锁获取方式 | 显式调用lock()/unlock() | 隐式(代码块 / 方法) |
| 可中断性 | 支持(lockInterruptibly()) | 不支持(等待时不可中断) |
| 超时机制 | 支持(tryLock(time)) | 不支持 |
| 公平性 | 可选择(公平 / 非公平) | 非公平 |
| 条件队列 | 多个(Condition) | 1 个 |
| 性能 | 高并发下更优(减少内核态切换) | JDK1.6 后优化,性能接近,但灵活性低 |
- Condition 的 await () 为什么要释放锁?
若不释放锁,其他线程无法获取锁,导致signal()永远无法被调用,等待的线程会永久阻塞。释放锁是为了让其他线程有机会满足条件并唤醒等待线程,实现协作。
结语:JUC 同步机制的设计哲学
AQS、Lock、Condition 的设计体现了 “分层抽象” 与 “模板方法” 的思想:
- AQS 将同步的共性逻辑(队列、阻塞)抽象出来,通过钩子函数适配不同场景;
- Lock 接口定义了锁的行为规范,具体实现委托给 AQS 的子类;
- Condition 则基于 AQS 的扩展能力,实现更灵活的线程协作。
这种设计让 JUC 的同步工具既统一又灵活,既避免重复开发,又能满足多样化的并发需求。理解它们的底层原理,不仅能更高效地使用ReentrantLock等工具,更能在复杂场景下设计自定义同步工具(如基于 AQS 实现分布式锁)。对于 Java 开发者而言,这是从 “会用 API” 到 “理解设计思想” 的关键一跃。