吃透Java Lock体系:ReentrantLock、AQS、CAS底层原理全解析
在Java并发编程中,synchronized 作为JVM内置锁,虽简单易用,但在复杂并发场景(如公平锁、可中断锁、超时获取锁、多条件唤醒)中灵活性不足。JDK 5 引入的 java.util.concurrent.locks 包(简称JUC锁),以 Lock 接口为核心,ReentrantLock 为主要实现,底层依托 AQS(抽象队列同步器) 和 CAS(比较并交换) 两大核心技术,实现了更灵活、高效的同步机制。
本文将从「使用场景→API细节→底层原理→源码解析→实战避坑」,全方位拆解 Lock 体系,不仅讲清“是什么”“怎么用”,更深入剖析“为什么这么设计”,帮你彻底掌握Java并发锁的核心逻辑,应对面试高频考点和实际开发难题。
一、Lock 接口:JDK层面的锁规范(比synchronized更灵活)
Lock 是JDK提供的顶层锁接口,定义了锁的核心操作规范,区别于 synchronized(JVM内置,隐式操作),Lock 是显式锁,需手动控制锁的获取与释放,同时提供了 synchronized 不具备的高级特性。
1.1 Lock 接口完整API
| 方法 | 核心作用 | 使用场景 | 注意事项 |
|---|---|---|---|
void lock() |
获取锁,若锁被占用则阻塞,直到获取到锁 | 基础同步场景,无需中断、超时 | 不可中断,若线程被阻塞,只能等待锁释放 |
void lockInterruptibly() throws InterruptedException |
获取锁,可响应线程中断,中断时抛出异常 | 避免线程永久阻塞(如超时任务、可取消任务) | 必须捕获 InterruptedException,处理中断逻辑 |
boolean tryLock() |
尝试获取锁,非阻塞,立即返回结果(成功true/失败false) | 无锁竞争时快速获取,避免阻塞 | 获取失败不阻塞,需手动判断是否重试 |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException |
超时获取锁,在指定时间内未获取到则返回false,支持中断 | 防止线程无限等待(如网络请求、资源获取) | 时间单位需明确,中断时抛出异常 |
void unlock() |
释放锁,必须手动调用 | 所有获取锁的场景,必写操作 | 必须在 finally 中调用,防止锁泄漏;未获取锁时调用抛异常 |
Condition newCondition() |
创建条件变量,实现线程的精准唤醒 | 多条件等待/唤醒(如生产者-消费者模型) | Condition 的 await()/signal() 需在锁范围内调用 |
1.2 Lock 与 synchronized 深度对比
除了基础特性对比,补充二者在底层实现、性能、适用场景的核心差异,帮你精准选择:
| 特性 | synchronized | Lock(ReentrantLock) | 补充说明 |
|---|---|---|---|
| 锁实现层面 | JVM 内置(C++ 底层实现,依赖对象头) | JDK 层面(Java 代码实现,依赖 AQS) | synchronized 由JVM自动管理锁生命周期,Lock 需手动管理 |
| 释放方式 | 自动释放(方法退出、代码块结束、异常抛出) | 手动释放(必须在 finally 中调用 unlock()) | Lock 忘记释放会导致锁泄漏,synchronized 无此问题 |
| 中断性 | 不可中断(阻塞线程无法被中断,只能等待锁释放) | 支持可中断(lockInterruptibly()) | Lock 可避免线程永久阻塞,更适合高可用场景 |
| 超时获取 | 不支持(无超时机制,线程可能无限等待) | 支持(tryLock 超时重载方法) | Lock 可设置超时时间,提升系统稳定性 |
| 公平锁支持 | 仅非公平锁(无法指定) | 支持公平/非公平锁(构造函数指定) | 公平锁需维护队列顺序,性能略低,非特殊场景不推荐 |
| 条件变量 | 单一条件(依赖 wait()/notify()/notifyAll()) | 多条件(多个 Condition 实例,精准唤醒) | synchronized 的 notifyAll() 会唤醒所有等待线程,Lock 可唤醒指定条件的线程 |
| 锁状态查询 | 不支持(无法判断锁是否被持有、重入次数) | 支持(isLocked()、getHoldCount()、isFair() 等) | Lock 可灵活查询锁状态,便于调试和监控 |
| 性能 | Java 6 后优化(偏向锁、轻量级锁),与 Lock 接近 | 高并发下略优,尤其是非公平锁场景 | 简单场景二者性能差异可忽略,复杂场景 Lock 更灵活 |
| 适用场景 | 简单同步场景(如单一线程安全变量修改) | 复杂并发场景(公平锁、可中断、超时、多条件唤醒) | 优先使用 synchronized(简单不易出错),复杂场景用 Lock |
1.3 Lock 接口的实现类(扩展)
Lock 接口并非只有 ReentrantLock 一个实现,还有其他常用实现类,覆盖不同场景:
-
ReentrantLock:可重入独占锁(最常用),支持公平/非公平模式,本文重点讲解;
-
ReentrantReadWriteLock.ReadLock/WriteLock:可重入读写锁,读锁共享、写锁独占,适合读多写少场景;
-
StampedLock:JDK 8 新增,优化读写锁性能,支持乐观读模式,解决读写锁的饥饿问题;
-
LockSupport:不是 Lock 接口实现,但提供了 park()/unpark() 方法,是 AQS 阻塞/唤醒线程的底层依赖。
二、ReentrantLock:Lock 接口的核心实现(可重入锁详解)
ReentrantLock 意为“可重入锁”,是 Lock 接口最核心、最常用的实现类,其核心特性与 synchronized 类似(可重入),但提供了更灵活的控制能力。底层基于 AQS 实现,支持公平锁和非公平锁两种模式。
2.1 ReentrantLock 核心构造函数
ReentrantLock 有两个核心构造函数,用于指定锁的公平性,底层通过 AQS 的子类实现:
// 1. 默认构造函数:非公平锁(性能优先)
public ReentrantLock() {
sync = new NonfairSync(); // 非公平锁同步器
}
// 2. 带参数构造函数:指定公平/非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync(); // 公平/非公平同步器
}
关键细节:
-
sync是 ReentrantLock 的内部类,继承自 AQS,分为FairSync(公平锁同步器)和NonfairSync(非公平锁同步器); -
公平锁和非公平锁的核心差异,在于
tryAcquire(int arg)方法的实现(AQS 的模板方法,由子类实现)。
2.2 ReentrantLock 基础使用
2.2.1 基本用法(非公平锁,最常用)
核心要点:lock() 获取锁,finally 中 unlock() 释放锁,避免锁泄漏。
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockBasicDemo {
// 1. 创建非公平可重入锁(默认)
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
// 同步方法:修改共享变量
public void increment() {
// 2. 获取锁(阻塞式)
lock.lock();
try {
// 临界区代码:保证原子性、可见性、有序性
count++;
System.out.println(Thread.currentThread().getName() + ":count=" + count);
// 模拟临界区耗时操作
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
} finally {
// 3. 释放锁(必须在finally中,无论是否异常)
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockBasicDemo demo = new ReentrantLockBasicDemo();
// 10个线程,每个执行10次increment,测试线程安全
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 10; j++) {
demo.increment();
}
}, "线程" + (i+1)).start();
}
}
}
2.2.2 公平锁用法(场景:避免线程饥饿)
公平锁的核心是“先到先得”,线程获取锁时,会先检查同步队列是否有等待线程,有则入队,无则尝试获取锁。适合对“公平性”要求高的场景(如金融交易、任务调度)。
public class ReentrantLockFairDemo {
// 构造函数指定公平锁
private final ReentrantLock fairLock = new ReentrantLock(true);
private int count = 0;
public void increment() {
fairLock.lock();
try {
count++;
System.out.println(Thread.currentThread().getName() + ":count=" + count + ",是否公平锁:" + fairLock.isFair());
} finally {
fairLock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockFairDemo demo = new ReentrantLockFairDemo();
// 3个线程竞争锁,观察执行顺序(公平锁会按线程启动顺序执行)
new Thread(() -> {
for (int i = 0; i < 3; i++) demo.increment();
}, "公平线程1").start();
new Thread(() -> {
for (int i = 0; i < 3; i++) demo.increment();
}, "公平线程2").start();
new Thread(() -> {
for (int i = 0; i < 3; i++) demo.increment();
}, "公平线程3").start();
}
}
执行结果特点:线程会按启动顺序依次获取锁,不会出现某个线程频繁抢锁的情况(非公平锁可能出现)。
2.2.3 可中断获取锁(场景:取消耗时任务)
lockInterruptibly() 允许线程在获取锁的过程中被中断,避免线程永久阻塞。例如:用户取消任务时,中断正在等待锁的线程。
public class ReentrantLockInterruptDemo {
private final ReentrantLock lock = new ReentrantLock();
// 可中断获取锁的方法
public void doTask() throws InterruptedException {
// 若线程被中断,会抛出InterruptedException
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + " 获取锁成功,执行任务");
// 模拟耗时任务
Thread.sleep(5000);
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁");
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockInterruptDemo demo = new ReentrantLockInterruptDemo();
// 线程1:获取锁后执行耗时任务
Thread t1 = new Thread(() -> {
try {
demo.doTask();
} catch (InterruptedException e) {
System.out.println("线程1被中断,停止执行任务");
}
}, "任务线程1");
// 线程2:尝试获取锁,然后中断线程1
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000); // 等待1秒,让t1先获取锁
t1.interrupt(); // 中断t1
System.out.println("线程2中断线程1");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "中断线程2");
t1.start();
t2.start();
}
}
执行结果:线程1获取锁后执行任务,线程2中断线程1,线程1抛出异常并释放锁,避免永久阻塞。
2.2.4 超时获取锁(场景:防止线程无限等待)
tryLock(long time, TimeUnit unit) 允许线程在指定时间内尝试获取锁,超时未获取则返回false,适合网络请求、资源获取等场景(避免因资源不可用导致线程卡死)。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTimeoutDemo {
private final ReentrantLock lock = new ReentrantLock();
// 超时获取锁的方法
public boolean tryDoTask(long timeout) {
try {
// 超时1秒获取锁,失败则返回false
if (lock.tryLock(timeout, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 获取锁成功,执行任务");
Thread.sleep(2000); // 模拟任务耗时
return true;
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " 获取锁超时,放弃执行");
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
public static void main(String[] args) {
ReentrantLockTimeoutDemo demo = new ReentrantLockTimeoutDemo();
// 线程1:先获取锁,执行2秒任务
new Thread(() -> demo.tryDoTask(1), "线程1").start();
// 线程2:1秒后尝试获取锁,超时时间1秒(此时线程1还在执行,线程2超时)
new Thread(() -> {
try {
Thread.sleep(1000);
demo.tryDoTask(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "线程2").start();
}
}
2.2.5 多条件唤醒(Condition 用法,场景:生产者-消费者)
ReentrantLock 的 newCondition() 方法可创建多个条件变量,实现线程的精准唤醒,解决 synchronized 中 notifyAll() 唤醒所有线程的低效问题。
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
// 生产者-消费者模型:用 Condition 精准唤醒
public class ReentrantLockConditionDemo {
private final ReentrantLock lock = new ReentrantLock();
// 两个条件变量:生产者等待(队列满)、消费者等待(队列空)
private final Condition producerCondition = lock.newCondition();
private final Condition consumerCondition = lock.newCondition();
private final Queue<Integer> queue = new LinkedList<>();
private final int MAX_CAPACITY = 5; // 队列最大容量
// 生产者:生产数据,队列满则等待
public void produce(int data) throws InterruptedException {
lock.lock();
try {
// 队列满,生产者等待
while (queue.size() == MAX_CAPACITY) {
System.out.println("队列满,生产者等待");
producerCondition.await(); // 释放锁,进入等待状态
}
// 生产数据
queue.offer(data);
System.out.println("生产者生产:" + data + ",队列大小:" + queue.size());
// 唤醒消费者(队列有数据了)
consumerCondition.signal();
} finally {
lock.unlock();
}
}
// 消费者:消费数据,队列空则等待
public int consume() throws InterruptedException {
lock.lock();
try {
// 队列空,消费者等待
while (queue.isEmpty()) {
System.out.println("队列空,消费者等待");
consumerCondition.await(); // 释放锁,进入等待状态
}
// 消费数据
int data = queue.poll();
System.out.println("消费者消费:" + data + ",队列大小:" + queue.size());
// 唤醒生产者(队列有空闲了)
producerCondition.signal();
return data;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockConditionDemo demo = new ReentrantLockConditionDemo();
// 3个生产者线程
for (int i = 0; i < 3; i++) {
int finalI = i;
new Thread(() -> {
try {
for (int j = 0; j < 3; j++) {
demo.produce(finalI * 10 + j);
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "生产者" + (i+1)).start();
}
// 2个消费者线程
for (int i = 0; i < 2; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 5; j++) {
demo.consume();
Thread.sleep(800);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "消费者" + (i+1)).start();
}
}
}
关键优势:生产者等待时,仅唤醒消费者;消费者等待时,仅唤醒生产者,避免不必要的线程唤醒,提升性能。
2.3 ReentrantLock 核心特性(深入解析)
2.3.1 可重入性
可重入性是指:同一线程获取锁后,可再次获取同一锁(无需释放),底层通过 AQS 的 state 变量记录重入次数。
public class ReentrantLockReentrantDemo {
private final ReentrantLock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
System.out.println("进入method1,重入次数:" + lock.getHoldCount());
method2(); // 同一线程重复获取锁
} finally {
lock.unlock();
System.out.println("退出method1,重入次数:" + lock.getHoldCount());
}
}
public void method2() {
lock.lock();
try {
System.out.println("进入method2,重入次数:" + lock.getHoldCount());
method3(); // 再次重入
} finally {
lock.unlock();
System.out.println("退出method2,重入次数:" + lock.getHoldCount());
}
}
public void method3() {
lock.lock();
try {
System.out.println("进入method3,重入次数:" + lock.getHoldCount());
} finally {
lock.unlock();
System.out.println("退出method3,重入次数:" + lock.getHoldCount());
}
}
public static void main(String[] args) {
ReentrantLockReentrantDemo demo = new ReentrantLockReentrantDemo();
demo.method1();
// 输出:
// 进入method1,重入次数:1
// 进入method2,重入次数:2
// 进入method3,重入次数:3
// 退出method3,重入次数:2
// 退出method2,重入次数:1
// 退出method1,重入次数:0
}
}
底层原理(结合 AQS):
-
AQS 的
state变量(volatile int)记录锁的重入次数,初始值为 0; -
线程第一次获取锁:CAS 将 state 从 0 改为 1,设置
exclusiveOwnerThread为当前线程; -
线程再次获取锁:判断当前线程是否为
exclusiveOwnerThread,若是则 state+1(重入次数增加); -
线程释放锁:state-1,当 state=0 时,清空
exclusiveOwnerThread,真正释放锁。
2.3.2 公平锁与非公平锁的底层差异(源码对比)
公平锁和非公平锁的核心差异,在于 tryAcquire(int arg) 方法的实现(AQS 的模板方法,由 ReentrantLock 的内部类实现)。
1. 非公平锁(NonfairSync)的 tryAcquire 实现
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// 尝试获取独占锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// 非公平锁获取逻辑(核心)
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 1. 若锁未被持有(state=0),直接CAS抢锁
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 2. 若当前线程已持有锁,重入(state+1)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 3. 锁被其他线程持有,返回false(入队等待)
return false;
}
非公平锁逻辑:线程尝试获取锁时,先直接 CAS 抢锁(不管队列中是否有等待线程),抢锁失败再入队,性能更高,但可能导致线程饥饿(某些线程一直抢不到锁)。
2. 公平锁(FairSync)的 tryAcquire 实现
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 关键差异:先检查队列是否有等待线程,无则CAS抢锁
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 重入逻辑与非公平锁一致
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
// 检查同步队列中是否有前驱线程(是否有线程比当前线程先等待)
public final boolean hasQueuedPredecessors() {
Node t = tail; // 队列尾节点
Node h = head; // 队列头节点
Node s;
// 头节点 != 尾节点 → 队列非空
// 队列头节点的后继节点 != 当前线程 → 有前驱线程
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
公平锁逻辑:线程尝试获取锁时,先通过 hasQueuedPredecessors() 检查同步队列是否有等待线程,有则入队,无则 CAS 抢锁,保证“先到先得”,无线程饥饿,但性能略低(需维护队列顺序)。
三、底层核心:AQS(抽象队列同步器)深度解析
AQS(AbstractQueuedSynchronizer)是 JUC 锁的“骨架”,是所有 JUC 锁(ReentrantLock、CountDownLatch、Semaphore 等)的底层基础。AQS 封装了锁的核心逻辑(锁获取、锁释放、线程阻塞/唤醒、同步队列管理),开发者只需实现少量抽象方法,即可自定义锁。
AQS 的核心设计思想:基于状态变量(state)+ 同步队列(CLH队列)+ CAS 操作,实现锁的独占/共享获取与释放。
3.1 AQS 核心结构(源码级拆解)
3.1.1 核心变量(AQS 类的核心字段)
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
// 序列化版本号
private static final long serialVersionUID = 7373984972572414691L;
// 同步队列的头节点(CLH队列)
private transient volatile Node head;
// 同步队列的尾节点(CLH队列)
private transient volatile Node tail;
// 锁状态变量:volatile修饰,保证可见性和有序性
private volatile int state;
// 构造函数(空参)
protected AbstractQueuedSynchronizer() {}
// 其他核心方法...
}
关键变量详解:
-
state:volatile int 类型,核心状态变量,不同锁的含义不同:
-
ReentrantLock(独占锁):state=0 表示未锁定,state>0 表示已锁定(值为锁的重入次数);
-
Semaphore(共享锁):state 表示可用许可数;
-
CountDownLatch(共享锁):state 表示剩余计数,countDown() 减1,await() 等待 state=0。
head/tail:volatile Node 类型,指向同步队列(CLH队列)的头节点和尾节点,CLH队列是一个双向链表,用于存储等待获取锁的线程。
Node 节点:AQS 的内部类,封装了等待线程、等待状态、前驱/后继节点,是同步队列的基本单元。
3.1.2 Node 节点结构(核心)
static final class Node {
// 共享模式标记
static final Node SHARED = new Node();
// 独占模式标记
static final Node EXCLUSIVE = null;
// 节点状态:等待状态,用于控制线程的阻塞/唤醒
volatile int waitStatus;
// 状态值:
// 1. CANCELLED = 1:线程被取消(如超时、中断),从队列中移除
// 2. SIGNAL = -1:当前节点的后继节点需要被唤醒
// 3. CONDITION = -2:线程在条件变量上等待(Condition.await())
// 4. PROPAGATE = -3:共享模式下,唤醒会传播给后续节点
// 5. 0:默认状态(无特殊含义)
// 前驱节点
volatile Node prev;
// 后继节点
volatile Node next;
// 当前节点对应的线程
volatile Thread thread;
// 条件变量相关的节点(Condition 用法)
Node nextWaiter;
// 其他方法...
}
3.1.3 AQS 核心模式(独占/共享)
AQS 支持两种核心模式,对应不同的锁场景:
1. 独占模式(Exclusive Mode)
同一时间仅一个线程能获取锁,其他线程需入队等待。适用于“排他性访问”场景(如 ReentrantLock、ReentrantReadWriteLock 的写锁)。
核心方法(模板方法,由子类实现):
-
tryAcquire(int arg):尝试获取独占锁,成功返回true,失败返回false; -
tryRelease(int arg):尝试释放独占锁,成功返回true,失败返回false; -
isHeldExclusively():判断当前线程是否持有独占锁。
2. 共享模式(Shared Mode)
多个线程可同时获取锁,只要锁的状态允许(如 Semaphore 的许可数>0)。适用于“共享访问”场景(如 CountDownLatch、Semaphore、ReentrantReadWriteLock 的读锁)。
核心方法(模板方法,由子类实现):
-
tryAcquireShared(int arg):尝试获取共享锁,返回>=0 表示成功(返回值为剩余许可数),返回<0 表示失败; -
tryReleaseShared(int arg):尝试释放共享锁,成功返回true,失败返回false; -
doAcquireShared(int arg):共享模式下,获取锁失败后入队等待; -
doReleaseShared(int arg):共享模式下,释放锁后唤醒后续节点。
3.2 AQS 核心流程(以 ReentrantLock 独占锁为例,完整链路)
AQS 的核心流程分为“锁获取”和“锁释放”两大环节,结合 CAS 和同步队列,实现线程的阻塞与唤醒。
3.2.1 锁获取流程(lock() 方法调用链路)
ReentrantLock.lock() → AQS.acquire(1) → 子类 tryAcquire(1) → 入队阻塞 → 唤醒后再次尝试获取锁。
// 1. ReentrantLock.lock() 调用(非公平锁为例)
public void lock() {
sync.lock(); // 调用 NonfairSync.lock()
}
// 2. NonfairSync.lock() 实现
final void lock() {
// 先CAS抢锁(非公平锁特性)
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 抢锁失败,调用AQS的acquire方法
acquire(1);
}
// 3. AQS.acquire(int arg) 模板方法(核心)
public final void acquire(int arg) {
// 步骤1:尝试获取锁(tryAcquire由子类实现)
// 步骤2:获取失败,封装为Node节点入队(addWaiter)
// 步骤3:阻塞当前线程(acquireQueued)
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt(); // 若线程被中断,恢复中断状态
}
关键步骤拆解:
-
tryAcquire(arg):子类实现(ReentrantLock 的公平/非公平逻辑),尝试获取锁,成功则返回true,失败则进入下一步;
-
addWaiter(Node.EXCLUSIVE):将当前线程封装为独占模式的 Node 节点,通过 CAS 加入同步队列尾部(保证入队原子性);
-
若队列为空(head/tail 为null),先初始化队列(创建头节点);
-
若队列非空,CAS 将新节点设为尾节点,同时将原尾节点的 next 指向新节点。
-
acquireQueued(Node node, int arg):让节点在队列中等待,直到获取到锁或被中断;
-
循环检查当前节点的前驱节点是否为头节点,若是则再次尝试获取锁;
-
若获取锁成功,将当前节点设为头节点,释放原头节点;
-
若获取锁失败,根据前驱节点的状态,决定是否阻塞当前线程(LockSupport.park());
-
线程被唤醒后,再次进入循环,尝试获取锁。
AQS 独占锁获取流程图(核心链路)
流程图说明:整个流程围绕“尝试获取→失败入队→阻塞等待→唤醒重试”展开,CAS保证入队和锁状态修改的原子性,volatile保证state和队列节点的可见性,最终实现线程的安全同步。
3.2.2 锁释放流程(unlock() 方法调用链路)
锁释放流程相对简单,核心是“释放锁状态→唤醒后续等待线程”,ReentrantLock.unlock() → AQS.release(1) → 子类 tryRelease(1) → 唤醒队列中的后继线程。
// 1. ReentrantLock.unlock() 调用
public void unlock() {
sync.release(1); // 调用AQS的release方法
}
// 2. AQS.release(int arg) 模板方法(核心)
public final boolean release(int arg) {
// 步骤1:尝试释放锁(tryRelease由子类实现)
if (tryRelease(arg)) {
// 步骤2:释放成功,获取头节点
Node h = head;
// 步骤3:头节点不为空且状态不是0,唤醒后继节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继线程
return true;
}
return false;
}
// 3. ReentrantLock的tryRelease实现(Sync类中)
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 只有持有锁的线程才能释放锁
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 重入次数减为0,真正释放锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null); // 清空持有线程
}
setState(c); // 更新state状态
return free;
}
// 4. 唤醒后继节点(AQS核心方法)
private void unparkSuccessor(Node node) {
// 获取当前节点状态
int ws = node.waitStatus;
// 若状态为SIGNAL(-1),重置为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 找到后继节点(跳过已取消的节点)
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 从尾节点向前找,找到最前面未取消的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 唤醒后继节点的线程
if (s != null)
LockSupport.unpark(s.thread);
}
关键步骤拆解:
-
tryRelease(arg):子类实现,核心是减少 state 重入次数,当 state=0 时,清空独占线程,标志锁完全释放;
-
unparkSuccessor(h):找到头节点的后继节点(跳过已取消的节点),通过 LockSupport.unpark() 唤醒该线程,被唤醒的线程会再次进入 acquireQueued 循环,尝试获取锁;
-
锁释放的核心原则:只有持有锁的线程才能释放锁,且必须释放到 state=0 才算真正释放,避免锁泄漏。
AQS 独占锁释放流程图(核心链路)
3.3 AQS 核心设计亮点(面试高频)
AQS 之所以能成为 JUC 锁的核心骨架,其设计亮点主要体现在以下3点,也是面试中常考的核心考点:
-
模板方法模式:AQS 封装了锁的通用逻辑(入队、阻塞、唤醒、状态管理),定义了抽象方法(tryAcquire、tryRelease 等),由子类(ReentrantLock 的 Sync 内部类)实现具体的锁逻辑,降低了自定义锁的开发成本,同时保证了锁的核心一致性;
-
CAS + volatile 保证线程安全:state 变量用 volatile 修饰,保证可见性和有序性;队列节点的入队、状态修改用 CAS 操作,保证原子性,避免了 synchronized 的阻塞开销,提升了高并发场景下的性能;
-
CLH 双向队列高效管理等待线程:CLH 队列是一种双向链表,采用“先进先出”(FIFO)原则,避免线程饥饿(公平锁),同时通过节点状态(SIGNAL、CANCELLED 等)过滤无效节点,减少不必要的线程唤醒,提升效率。
四、底层基石:CAS(比较并交换)深度解析
CAS(Compare And Swap,比较并交换)是 AQS、ReentrantLock 等并发组件的底层原子操作,是实现“无锁编程”的核心技术,也是解决并发安全问题的关键,其核心思想是“无锁竞争”,比 synchronized 更轻量。
4.1 CAS 核心定义与原理
CAS 是一种原子操作(不可中断、不可分割),其操作逻辑如下:
给定三个参数:内存地址 V、预期值 A、新值 B;
当且仅当内存地址 V 中的值等于预期值 A 时,将 V 中的值更新为 B,否则不做任何操作;
无论操作成功与否,都返回 V 中原来的值。
简单来说:“我认为内存里的值是 A,要是真的是 A,就改成 B,不然就不改”,整个过程原子完成,无需加锁。
4.1.1 CAS 底层实现(JDK + CPU 层面)
CAS 并非 Java 层面的实现,而是依赖 CPU 的原子指令(不同 CPU 指令不同):
-
x86 架构:CPU 提供
cmpxchg指令(compare and exchange),实现 CAS 原子操作; -
Java 层面:通过
sun.misc.Unsafe类调用 native 方法,底层调用 CPU 原子指令; -
核心 API:Unsafe 类中的
compareAndSwapInt(Object o, long offset, int expected, int update),分别对应“对象、内存偏移量、预期值、新值”。
// AQS 中 CAS 修改 state 的核心代码(简化)
private final boolean compareAndSetState(int expect, int update) {
// 调用 Unsafe 的 CAS 方法,修改 state 变量
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
关键细节:stateOffset 是 state 变量在 AQS 类中的内存偏移量,通过 Unsafe 类的 objectFieldOffset 方法获取,确保能精准定位到 state 变量的内存地址。
4.2 CAS 的优势与缺陷
4.2.1 优势
-
无锁竞争,性能高效:CAS 是无锁操作,无需阻塞线程,减少了线程上下文切换的开销(synchronized 阻塞线程会导致上下文切换),高并发场景下性能优于 synchronized;
-
原子性保证:依赖 CPU 原子指令,确保操作不可分割,避免并发安全问题;
-
轻量级:无需 JVM 层面的锁管理(如 synchronized 的偏向锁、轻量级锁切换),底层直接操作内存,开销小。
4.2.2 缺陷(面试高频考点)
-
ABA 问题:
定义:内存地址 V 中的值从 A 改成 B,再改回 A,CAS 操作会认为“值未变”,从而执行更新操作,可能导致并发安全问题; -
解决方式:使用“版本号机制”(如 AtomicStampedReference),给每个值关联一个版本号,更新时不仅比较值,还比较版本号,避免 ABA 问题。
-
自旋开销:
定义:CAS 操作失败时,线程会循环重试(自旋),直到操作成功;若高并发下大量线程自旋,会占用 CPU 资源,导致 CPU 使用率飙升; -
解决方式:设置自旋次数上限(如 AQS 中的自旋次数限制),或搭配阻塞机制(自旋失败后阻塞线程),平衡性能和 CPU 开销。
-
只能保证单个变量的原子操作:CAS 只能对单个变量执行原子操作,无法实现多个变量的原子性(如同时修改两个变量);
-
解决方式:使用
AtomicReference包装多个变量(如自定义对象),或使用 synchronized 保证多个变量的原子性。
4.3 CAS 在 AQS 中的应用(核心场景)
AQS 中大量使用 CAS 操作,核心应用场景有3个:
-
修改 state 变量:如 ReentrantLock 中,CAS 将 state 从 0 改为 1(获取锁)、从 1 改为 0(释放锁),保证锁状态修改的原子性;
-
同步队列节点入队:addWaiter 方法中,CAS 将新节点设为尾节点,保证多个线程同时入队时的原子性,避免队列混乱;
-
修改节点状态:如 unparkSuccessor 方法中,CAS 将节点的 waitStatus 从 SIGNAL(-1)改为 0,保证节点状态修改的原子性。
五、实战避坑:ReentrantLock、AQS、CAS 常见问题与解决方案
结合实际开发和面试高频问题,整理核心避坑点,帮你规避并发bug,加深对底层原理的理解。
5.1 ReentrantLock 常见坑
坑1:忘记在 finally 中释放锁,导致锁泄漏
现象:线程获取锁后,因异常或逻辑遗漏未释放锁,导致其他线程永久阻塞,最终引发系统死锁;
解决方案:必须在 finally 块中调用 unlock() 方法,无论是否发生异常,都能保证锁释放;
// 错误示例(无finally释放锁)
public void wrongMethod() {
lock.lock();
// 临界区代码,若抛出异常,锁无法释放
count++;
lock.unlock(); // 异常后不会执行
}
// 正确示例(finally释放锁)
public void rightMethod() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 无论是否异常,必执行
}
}
坑2:公平锁滥用,导致性能下降
现象:为了“公平性”,盲目使用公平锁,导致线程频繁入队等待,吞吐量下降;
解决方案:非特殊场景(如金融交易、任务调度),优先使用默认的非公平锁,平衡性能和公平性;
核心原因:公平锁需每次检查同步队列,增加了额外开销,非公平锁的“抢锁”机制更高效。
坑3:多线程调用 unlock(),导致 IllegalMonitorStateException
现象:未获取锁的线程调用 unlock(),抛出异常;或同一线程多次释放锁(超过重入次数);
解决方案:
-
确保只有获取锁的线程才能调用 unlock();
-
使用 getHoldCount() 方法查询重入次数,避免多次释放。
5.2 AQS 常见问题(面试高频)
问题1:AQS 的 state 变量为什么用 volatile 修饰?
解答:保证 state 的可见性和有序性:
-
可见性:一个线程修改 state 后,其他线程能立即看到最新值,避免因缓存导致的锁状态不一致;
-
有序性:禁止指令重排序,确保 state 的修改和读取顺序符合代码逻辑,避免“锁已释放但线程仍阻塞”的问题。
问题2:AQS 的同步队列为什么是双向链表?
解答:双向链表便于节点的删除和唤醒操作:
-
删除节点:当线程被取消(CANCELLED 状态),可通过前驱节点快速移除当前节点,避免队列冗余;
-
唤醒后继:释放锁时,需找到头节点的后继节点,双向链表可快速定位前驱和后继,提升唤醒效率。
问题3:AQS 的 park() 和 unpark() 与 Thread.sleep() 的区别?
解答:核心区别在于“是否释放锁”和“唤醒机制”:
-
LockSupport.park():阻塞线程时,不会释放已获取的锁,可被 unpark() 直接唤醒,也可被中断唤醒;
-
Thread.sleep(long ms):阻塞线程时,不会释放锁,只能等待超时或被中断唤醒,无法被主动唤醒;
-
应用场景:AQS 中用 park()/unpark() 实现线程的阻塞与唤醒,因为可主动唤醒,更灵活,适合锁的等待场景。
5.3 CAS 常见问题(面试高频)
问题1:ABA 问题的具体场景的解决方案?
示例场景:线程1准备将变量从 A 改为 B,线程2先将 A 改为 C,再改回 A,线程1 CAS 操作认为值未变,执行修改,导致数据不一致;
解决方案:使用 AtomicStampedReference,给变量关联版本号,更新时同时比较值和版本号:
// AtomicStampedReference 解决 ABA 问题
AtomicStampedReference<Integer> atomicStamped = new AtomicStampedReference<>(1, 1); // 初始值1,版本号1
// 线程1尝试更新:预期值1,新值2,预期版本号1,新版本号2
boolean success = atomicStamped.compareAndSet(1, 2, 1, 2);
// 若线程2修改了值和版本号,线程1的CAS会失败
问题2:CAS 自旋次数过多导致 CPU 飙升,如何解决?
解决方案:
-
设置自旋次数上限:如 AQS 中,自旋一定次数(如10次)后,若仍未获取锁,就阻塞线程,避免无限自旋;
-
搭配阻塞机制:自旋失败后,调用 LockSupport.park() 阻塞线程,减少 CPU 占用;
-
使用自适应自旋:根据前一次自旋的结果,动态调整自旋次数(如前一次成功,增加自旋次数;前一次失败,减少自旋次数)。
六、总结:Lock 体系核心逻辑梳理(面试必背)
本文从 Lock 接口入手,逐步拆解 ReentrantLock、AQS、CAS 的底层原理,核心逻辑可总结为“三层架构”:
-
上层:Lock 接口:定义锁的规范,提供灵活的锁操作(可中断、超时、多条件唤醒),是开发者直接使用的入口;
-
中层:ReentrantLock:Lock 接口的核心实现,基于 AQS 实现公平/非公平锁、可重入性,封装了锁的具体逻辑;
-
底层:AQS + CAS:AQS 是锁的骨架,封装了队列管理、线程阻塞/唤醒、状态管理;CAS 是原子操作基础,保证锁状态和队列操作的原子性,二者共同支撑起整个 Lock 体系。
核心考点提炼:
-
ReentrantLock 的公平/非公平锁差异、可重入性原理;
-
AQS 的核心结构(state、CLH队列、Node节点)、核心流程(锁获取/释放);
-
CAS 的原理、优势、缺陷及解决方案;
-
Lock 与 synchronized 的对比,适用场景选择。
更多推荐

所有评论(0)