第27题:实现一把锁的思路
📚 回答:
- 核心考点: 实现一把锁是 Java 并发编程的终极试金石,大厂面试不会只问"用 CAS 设置状态位",而是深入考察 锁的完整语义(互斥、可重入、公平性、可中断)、AQS 的 CLH 变体队列设计、自旋与阻塞的权衡、内存屏障的插入位置,以及 从简单自旋锁到工业级 AQS 锁的演进路径。面试官真正想判断的是:你是否能从"能跑"进化到"跑得好",理解锁设计的每一个工程决策背后的原因。
1. 锁的核心语义与设计要素
实现一把锁需要满足以下核心语义:
| 语义 | 说明 | 实现复杂度 |
|---|---|---|
| 互斥性 | 同一时刻只有一个线程持有锁 | ⭐ |
| 可重入性 | 同一线程可多次获取锁 | ⭐⭐ |
| 公平性 | 按请求顺序获取锁(FIFO) | ⭐⭐⭐ |
| 可中断性 | 等待锁时可被中断 | ⭐⭐⭐ |
| 超时获取 | 等待指定时间后放弃 | ⭐⭐⭐ |
| 条件变量 | 支持 await/signal 协作 | ⭐⭐⭐⭐ |
2. 方案一:简单自旋锁(Spin Lock)
- 2.1 原理
使用 AtomicBoolean + CAS 实现最基础的互斥锁。线程获取锁失败时自旋等待(忙等)。
public class SpinLock {
private final AtomicBoolean locked = new AtomicBoolean(false);
public void lock() {
// ★ CAS 自旋:预期 false,更新为 true
while (!locked.compareAndSet(false, true)) {
// 自旋等待,CPU 空转
}
}
public void unlock() {
locked.set(false); // 释放锁
}
}
- 2.2 优缺点分析
| 特性 | 说明 |
|---|---|
| 优点 | 实现简单,无线程切换开销,低竞争下性能极高 |
| 缺点 | 高竞争下 CPU 空转严重;无公平性保证;不可重入 |
| 适用场景 | 临界区极短(如计数器自增)、低竞争环境 |
- 2.3 优化:Ticket Lock(公平自旋锁)
public class TicketLock {
private final AtomicInteger ticket = new AtomicInteger(0); // 发号器
private final AtomicInteger owner = new AtomicInteger(0); // 当前服务号
public void lock() {
int myTicket = ticket.getAndIncrement(); // 取号
while (myTicket != owner.get()) {
// 自旋等待叫号
}
}
public void unlock() {
owner.incrementAndGet(); // 叫下一个号
}
}
优势:保证了 FIFO 公平性,避免了"后来者先服务"的饥饿问题。[citation:0]
3. 方案二:CLH 锁(队列自旋锁)
- 3.1 原理
CLH 锁(Craig, Landin, Hagersten)是一种 基于隐式链表 的公平自旋锁。每个线程有一个自己的节点,通过前驱节点的状态来判断是否获取锁。
public class CLHLock {
private final ThreadLocal<Node> myNode = ThreadLocal.withInitial(Node::new);
private final AtomicReference<Node> tail = new AtomicReference<>(new Node());
private static class Node {
volatile boolean locked = false; // true: 需要等待;false: 锁已释放
}
public void lock() {
Node node = myNode.get();
node.locked = true; // 标记自己需要等待
Node pred = tail.getAndSet(node); // 加入队列尾部,获取前驱
while (pred.locked) {
// ★ 自旋等待前驱节点释放锁(监听前驱状态,非全局状态)
}
}
public void unlock() {
Node node = myNode.get();
node.locked = false; // 释放锁
myNode.set(new Node()); // 为下次加锁准备新节点
}
}
- 3.2 CLH 锁的核心优势
| 优势 | 说明 |
|---|---|
| 缓存友好 | 每个线程只监听自己的前驱节点状态,不访问全局变量,减少缓存一致性流量 |
| 公平性 | 隐式队列保证 FIFO,无饥饿 |
| 可扩展性 | 适用于高并发多核系统,不会因为锁竞争而出现严重抖动 |
- 3.3 CLH vs MCS 锁
| 特性 | CLH 锁 | MCS 锁 |
|---|---|---|
| 自旋对象 | 前驱节点的状态 | 自己的节点状态 |
| 释放操作 | 修改自己的节点(前驱已离开) | 修改后继节点的状态 |
| 内存开销 | 较低(ThreadLocal 复用) | 较高(每个节点独立) |
| NUMA 友好 | 一般 | 更好(自旋本地内存) |
[citation:1]
4. 方案三:AQS 框架——工业级锁的实现(核心)
AQS(AbstractQueuedSynchronizer)是 Java 并发包的基石,ReentrantLock、Semaphore、CountDownLatch 全部基于它实现。理解 AQS 就等于掌握了 Java 并发框架的心脏。
- 4.1 AQS 核心设计
┌─────────────────────────────────────────┐
│ AQS 核心组件 │
├─────────────────────────────────────────┤
│ state (volatile int) │
│ ├── 独占模式:0=未锁定,1=锁定,>1=重入 │
│ └── 共享模式:剩余许可数 │
├─────────────────────────────────────────┤
│ CLH 变体队列(双向链表) │
│ ├── head:虚拟头节点(不存储线程) │
│ ├── tail:尾节点 │
│ └── Node:{waitStatus, prev, next, thread}│
├─────────────────────────────────────────┤
│ 模板方法(子类实现) │
│ ├── tryAcquire / tryRelease │
│ └── tryAcquireShared / tryReleaseShared │
└─────────────────────────────────────────┘
- 4.2 Node 节点的 waitStatus 状态
| 状态值 | 名称 | 含义 |
|---|---|---|
0 | 初始状态 | 节点刚创建时的状态 |
-1 | SIGNAL | 后继节点需要被唤醒 |
-2 | CONDITION | 节点在条件队列中等待 |
-3 | PROPAGATE | 共享模式下无条件传播 |
1 | CANCELLED | 节点被取消(超时或中断) |
- 4.3 独占锁获取流程(acquire)
// AQS.acquire() 核心逻辑
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 1. 尝试获取锁(子类实现)
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 2. 加入队列 + 自旋/阻塞
selfInterrupt(); // 3. 恢复中断标志
}
详细流程:
线程调用 acquire()
│
▼
tryAcquire(arg) ──成功?──→ 获取锁,返回
│ 失败
▼
addWaiter(Node.EXCLUSIVE) ──→ 创建节点,CAS 加入队列尾部
│
▼
acquireQueued(node, arg)
│
├── 前驱是 head?──→ tryAcquire() 再试一次
│ │ 成功 → 设置自己为 head,返回
│ │ 失败 → 继续
│
├── shouldParkAfterFailedAcquire() ──→ 检查前驱状态
│ │ 前驱 SIGNAL → 可以 park
│ │ 前驱 CANCELLED → 跳过,找有效前驱
│ │ 其他 → CAS 设置前驱为 SIGNAL
│
└── parkAndCheckInterrupt() ──→ LockSupport.park() 阻塞
│ 被 unpark/中断唤醒
└── 循环回到顶部,再次尝试获取锁
[citation:2]
- 4.4 独占锁释放流程(release)
// AQS.release() 核心逻辑
public final boolean release(int arg) {
if (tryRelease(arg)) { // 1. 尝试释放锁(子类实现)
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 2. 唤醒后继节点
return true;
}
return false;
}
unparkSuccessor 逻辑:
- 将 head 的 waitStatus 从 SIGNAL 改为 0。
- 从 tail 向前遍历,找到离 head 最近的有效后继节点。
- 调用
LockSupport.unpark()唤醒该节点。
- 4.5 自定义独占锁(基于 AQS)
public class CustomLock implements Lock {
private final Sync sync = new Sync();
// AQS 子类实现
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int acquires) {
// 非公平锁:直接 CAS 抢锁
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// ★ 可重入:state + 1
int nextc = c + acquires;
if (nextc < 0) throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
@Override
protected boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // ★ 重入次数归零才真正释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
@Override
protected boolean isHeldExclusively() {
return getState() != 0;
}
Condition newCondition() {
return new ConditionObject();
}
}
@Override public void lock() { sync.acquire(1); }
@Override public void unlock() { sync.release(1); }
@Override public Condition newCondition() { return sync.newCondition(); }
@Override public boolean tryLock() { return sync.tryAcquire(1); }
@Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
}
[citation:3]
5. 方案四:ReentrantLock 的公平与非公平实现
- 5.1 非公平锁(NonfairSync)
// ReentrantLock.NonfairSync.tryAcquire()
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// ★ 非公平:直接 CAS 抢锁,不管队列中是否有等待线程
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// ... 重入逻辑
return false;
}
特点:新来的线程可以和队列中的线程竞争锁,吞吐量更高,但可能导致饥饿。
- 5.2 公平锁(FairSync)
// ReentrantLock.FairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// ★ 公平:先检查队列中是否有前驱节点
if (!hasQueuedPredecessors() && // 队列中没有等待线程才能抢
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// ... 重入逻辑
return false;
}
特点:按 FIFO 顺序获取锁,无饥饿,但吞吐量略低。
- 5.3 性能对比
| 模式 | 吞吐量 | 公平性 | 适用场景 |
|---|---|---|---|
| 非公平锁 | 高(约 2~5 倍) | 不保证 | 通用场景,追求性能 |
| 公平锁 | 较低 | 严格 FIFO | 避免饥饿,如定时任务调度 |
[citation:4]
6. 锁的内存语义与 happens-before
- 6.1 锁获取的内存屏障
// ReentrantLock.lock() 的内存语义等价于:
// 1. 插入 LoadLoad + LoadStore 屏障(确保后续读写不会重排序到锁获取之前)
// 2. 从主内存加载共享变量的最新值
- 6.2 锁释放的内存屏障
// ReentrantLock.unlock() 的内存语义等价于:
// 1. 插入 StoreStore + StoreLoad 屏障(确保锁释放前的写操作对其他线程可见)
// 2. 将修改刷新到主内存
happens-before 规则:解锁操作 happens-before 后面对同一个锁的加锁操作。这意味着线程 A 解锁前对共享变量的修改,对线程 B 加锁后可见。
7. 生产环境避坑指南
- 7.1 不要自己实现锁,优先使用 JUC 工具
// ❌ 错误:自己实现锁,容易出 Bug
public class MyLock { ... }
// ✅ 正确:使用 JUC 提供的成熟工具
ReentrantLock lock = new ReentrantLock();
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
StampedLock stampedLock = new StampedLock();
- 7.2 锁的粒度要细
// ❌ 错误:大锁,串行化严重
public synchronized void process() {
// 读配置(只读,不需要锁)
Config config = loadConfig();
// 写数据库(需要锁)
writeDB(config);
// 发送通知(不需要锁)
sendNotify();
}
// ✅ 正确:细粒度锁
public void process() {
Config config = loadConfig(); // 无锁
synchronized (dbLock) {
writeDB(config);
}
sendNotify(); // 无锁
}
- 7.3 避免锁嵌套导致死锁
// ❌ 错误:嵌套锁,顺序不一致导致死锁
void methodA() {
lockA.lock();
lockB.lock(); // 可能死锁!
// ...
}
void methodB() {
lockB.lock();
lockA.lock(); // 可能死锁!
// ...
}
// ✅ 正确:全局统一的加锁顺序
void methodA() {
Lock first = lockA.hashCode() < lockB.hashCode() ? lockA : lockB;
Lock second = lockA.hashCode() < lockB.hashCode() ? lockB : lockA;
first.lock();
second.lock();
// ...
}
- 7.4 使用 try-finally 确保释放
// ❌ 错误:异常时锁不释放
lock.lock();
doWork(); // 抛异常 → 锁永远不释放!
lock.unlock();
// ✅ 正确:try-finally
lock.lock();
try {
doWork();
} finally {
lock.unlock(); // 确保释放
}
- 7.5 优先使用读写锁
// 读多写少场景
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读操作(并发)
rwLock.readLock().lock();
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
// 写操作(互斥)
rwLock.writeLock().lock();
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
8. 面试官追问与高分回答模板
- 追问 1:“实现一把锁的思路是什么?”
低分回答:“用 CAS 设置一个状态位,成功就获取锁,失败就自旋等待。”(太浅,没有触及队列管理和阻塞)
高分回答:
"实现一把锁需要分层次考虑:
第一层:互斥性
使用 CAS 操作竞争一个共享状态(如AtomicBoolean或volatile int)。成功则获取锁,失败则进入等待逻辑。第二层:等待管理
竞争失败的线程不能一直自旋(CPU 空转),需要进入等待队列。有两种选择:
- 自旋锁:适用于临界区极短、低竞争场景,如 CLH 锁通过监听前驱节点状态减少缓存一致性流量。
- 阻塞锁:使用
LockSupport.park()挂起线程,由unpark()唤醒。这是 AQS 的选择。第三层:队列管理
使用 CLH 变体队列(双向链表)管理等待线程。每个线程封装为 Node 节点,维护waitStatus(SIGNAL/CANCELLED 等)、前驱/后继指针。AQS 通过shouldParkAfterFailedAcquire()清理 CANCELLED 节点,确保队列健康。第四层:高级语义
- 可重入性:通过
state计数 +exclusiveOwnerThread记录持有线程。- 公平性:公平锁在
tryAcquire()中先检查hasQueuedPredecessors()。- 可中断/超时:
acquireInterruptibly()和tryAcquireNanos()支持响应中断和超时。- 条件变量:
ConditionObject维护独立的条件队列,实现await/signal。工业级实现直接继承 AQS,重写
tryAcquire/tryRelease即可,如ReentrantLock。"
- 追问 2:“AQS 的 CLH 队列和原始 CLH 锁有什么区别?”
高分回答:
"AQS 的队列是 CLH 的变体,有三点核心差异:
- 双向链表 vs 单向链表:原始 CLH 是隐式单向链表(通过 ThreadLocal 的前驱引用),AQS 是显式双向链表(
prev+next),支持从尾部向前遍历清理 CANCELLED 节点。- 阻塞 vs 自旋:原始 CLH 是自旋锁(监听前驱状态),AQS 是自旋 + 阻塞混合(先自旋几次,失败则
park())。- 虚拟头节点:AQS 使用虚拟头节点(不存储线程),原始 CLH 没有。虚拟头节点简化了边界条件处理。
AQS 的设计权衡:自旋几次再 park,既避免了短临界区的线程切换开销,又避免了长临界区的 CPU 空转。"
- 追问 3:“为什么 AQS 用
LockSupport.park()而不是Object.wait()?”
高分回答:
"AQS 选择
LockSupport.park()而非Object.wait()有四个核心原因:
- 无需持有锁:
wait()必须在同步块内调用,AQS 的队列管理不需要与某个对象的 Monitor 绑定。- 精确唤醒:
unpark(thread)可以唤醒指定线程;notify()随机唤醒,notifyAll()产生惊群效应。AQS 需要精确唤醒队列中的后继节点。- permit 防信号丢失:
unpark()可以先于park()调用(permit 缓存),而notify()发送时无人等待则信号丢失。- 功能更丰富:支持超时
parkNanos()、不响应中断等,满足复杂锁的需求。底层实现上,
park()调用Unsafe.park(),在 Linux 下是pthread_cond_wait,Windows 下是WaitForSingleObject,直接操作线程调度器。"
- 追问 4:“ReentrantLock 的公平锁和非公平锁有什么区别?性能差距有多大?”
高分回答:
"核心区别在于
tryAcquire()的实现:
- 非公平锁:线程直接 CAS 抢锁,不管队列中是否有等待线程。新来的线程可以和队列中的线程竞争,吞吐量更高(约 2~5 倍),但可能导致队列中的线程长期饥饿。
- 公平锁:线程在
tryAcquire()前先调用hasQueuedPredecessors()检查队列中是否有前驱节点。如果有,即使锁空闲也不抢,按 FIFO 顺序获取。无饥饿,但吞吐量较低。性能差距:在 JDK 的基准测试中,非公平锁的吞吐量通常是公平锁的 2~5 倍。原因是公平锁的线程切换更频繁(每个线程获取锁后很快释放,下一个线程立即被唤醒),而非公平锁允许线程"连续获取",减少了切换。
适用场景:
- 非公平锁:通用场景,追求吞吐量(默认)。
- 公平锁:避免饥饿的场景,如定时任务调度、资源分配器。"
- 追问 5:“如何实现锁的可重入性?”
高分回答:
"可重入性通过两个字段实现:
state(int):记录锁的重入次数。state = 0表示未锁定,state > 0表示被锁定且值为重入次数。exclusiveOwnerThread(Thread):记录当前持有锁的线程。获取逻辑:
if (state == 0) { // 尝试 CAS 获取锁 } else if (current == exclusiveOwnerThread) { state++; // 同一线程重入 return true; }释放逻辑:
if (current != exclusiveOwnerThread) throw new IllegalMonitorStateException(); int c = state - 1; if (c == 0) { exclusiveOwnerThread = null; state = 0; // 真正释放 return true; // 需要唤醒后继 } state = c; // 只是减少重入次数 return false; // 不需要唤醒关键:
state归零时才真正释放锁,唤醒后继节点。"
- 追问 6:“如果让你设计一个支持 1000 并发的高性能锁,你会怎么设计?”
高分回答:
"1000 并发的高性能锁设计需要分层优化:
1. 锁类型选择
- 读多写少 →
ReentrantReadWriteLock或StampedLock(乐观读)。- 写多读少 →
ReentrantLock(非公平)。- 纯计数 →
LongAdder(分段 CAS,无锁)。2. 锁粒度优化
- 全局锁 → 分段锁(如
ConcurrentHashMap的 16 段)。- 按哈希值分锁:
locks[hash & (N-1)]。3. 自旋优化
- 短临界区:先自旋几次(如 AQS 的
spinForTimeoutThreshold),再 park。- 自适应自旋:根据历史成功率动态调整自旋次数。
4. 队列优化
- 使用 CLH 变体队列,减少缓存一致性流量。
- 批量唤醒:共享模式下传播唤醒信号,减少
unpark()调用。5. 无锁化
- 如果业务允许,使用 CAS 无锁算法(如
AtomicReference+ 版本号)替代锁。- 使用
VarHandle(JDK 9+)实现更底层的原子操作。6. 监控与调优
- 暴露锁竞争指标(等待时间、队列长度)。
- 动态调整公平性(根据负载切换公平/非公平模式)。"
9. 方案选型速查表
| 场景 | 推荐方案 | 核心理由 |
|---|---|---|
| 学习/面试 | 手写 CAS 自旋锁 → CLH 锁 → AQS 锁 | 理解锁的演进路径 |
| 生产环境通用 | ReentrantLock(非公平) | 成熟、高性能、功能丰富 |
| 避免饥饿 | ReentrantLock(公平) | 严格 FIFO |
| 读多写少 | ReentrantReadWriteLock | 读锁共享,写锁互斥 |
| 读极多写极少 | StampedLock(乐观读) | 乐观读无锁,性能更高 |
| 纯计数/累加 | LongAdder | 分段 CAS,无锁 |
| 高并发缓存 | 分段锁(如 CHM) | 16 段分散竞争 |
| 自定义同步器 | 继承 AQS | 只需实现 tryAcquire/tryRelease |
💡 面试官想要的满分总结:
实现一把锁不是"用 CAS 设置一个标志位"这么简单,而是需要 状态管理 + 队列管理 + 阻塞唤醒 + 高级语义 的完整系统。
演进路径:简单自旋锁(CAS)→ Ticket Lock(公平自旋)→ CLH 锁(队列自旋,缓存友好)→ AQS(自旋+阻塞混合,工业级)。
AQS 的核心设计:
state:同步状态,独占模式表示锁的重入次数,共享模式表示剩余资源。- CLH 变体队列:双向链表管理等待线程,
waitStatus控制唤醒/取消逻辑。- 模板方法:子类只需实现
tryAcquire/tryRelease,AQS 处理队列、阻塞、中断、超时。LockSupport.park/unpark:精确唤醒指定线程,无需锁,permit 防丢失。工程实践:
- 不要自己实现锁,优先使用 JUC 工具。
- 锁粒度要细,避免大锁。
- 用
try-finally确保释放。- 读多写少用读写锁或
StampedLock。面试中能讲清楚 CLH 队列的缓存优化、AQS 的 acquire/release 完整流程、公平/非公平的性能差异,就已经超越了 95% 的候选人。
觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯

1万+

被折叠的 条评论
为什么被折叠?



