锁是并发系统的灵魂,也是技术面试中永不落幕的核心考点。本文从并发问题的本质出发,系统梳理 Java 语言级锁、MySQL 数据库锁、分布式锁 三大体系,深入
synchronized锁升级、AQS源码、InnoDB间隙锁、Redisson看门狗等面试高频追问点,并给出每种锁在主流框架中的具体应用。
第一章 锁产生的背景:并发编程的三大之痛
1.1 为什么需要锁?
在单线程环境中,程序的执行顺序是确定的。但在多线程/多进程环境下,操作系统通过时间片轮转调度线程,导致同一时刻多个执行流可能访问共享资源。如果没有协调机制,就会引发三类问题:
| 问题 | 定义 | 典型案例 |
|---|---|---|
| 原子性破坏 | 一个操作被线程调度打断,未完整执行 | i++(读取→修改→写入三步非原子) |
| 可见性破坏 | 线程修改了变量,其他线程无法立即感知 | 主线程修改标志位,子线程循环无法退出 |
| 有序性破坏 | 编译器和 CPU 对指令重排序,导致执行顺序与代码不一致 | DCL 单例模式中对象半初始化 |
竞态条件(Race Condition) 是上述问题的具体表现:多个线程对同一数据进行读写,最终结果依赖于线程执行的精确时序,而非程序逻辑。
临界区(Critical Section) 是访问共享资源的代码片段。锁的本质,就是通过对临界区施加**互斥(Mutual Exclusion)**约束,使得同一时刻只有一个执行流进入临界区,从而保证原子性、可见性与有序性。
1.2 硬件层面的支撑
现代 CPU 为锁提供了底层原子指令:
- CAS(Compare-And-Swap) :比较内存值与预期值,相等则更新。是
Atomic类与轻量级锁的基石。 - MESI 缓存一致性协议 :保证多核 CPU 缓存的一致性,是
volatile可见性的硬件基础。 - 内存屏障(Memory Barrier) :阻止指令重排序,是
volatile有序性的硬件保障。
第二章 锁的全景分类与面试考点地图
在展开细节之前,先建立整体认知。锁可以从多个维度进行分类:
按作用域:
- 单机锁(进程内锁) :作用于单个 JVM/进程内部,如
synchronized、ReentrantLock。 - 分布式锁(跨进程锁) :作用于多个 JVM/机器之间,如
Redis Redisson、ZooKeeper Curator。
按策略:
- 悲观锁:先加锁,再操作。认为冲突必然发生。
- 乐观锁:不加锁,提交时检查版本号。认为冲突很少发生。
按特性:
- 公平锁:按请求顺序分配锁(FIFO)。
- 非公平锁:允许"插队",吞吐更高。
- 可重入锁 :同一线程可多次获取同一把锁(
synchronized、ReentrantLock)。 - 不可重入锁:同一线程再次获取会死锁。
- 共享锁(读锁):多个线程可同时持有。
- 排他锁(写锁):仅一个线程可持有。
按实现层次:
- 语言级 :Java
synchronized、ReentrantLock、StampedLock - 操作系统级 :
Mutex、Spinlock、Semaphore - 数据库级 :
InnoDB行锁、表锁、间隙锁 - 分布式协调级 :
Redis、ZooKeeper、etcd
面试考点地图:
- 初级 :
synchronized用法、volatile作用、乐观锁悲观锁区别 - 中级 :锁升级过程、
ReentrantLock与synchronized区别、AQS原理、MySQL 行锁与表锁 - 高级 :
InnoDB间隙锁与死锁、Redisson看门狗源码、Redlock争议、ZooKeeper临时有序节点
第三章 Java 语言级锁------JVM 与 JUC 核心
Java 并发锁是面试的最高频区域,占锁相关问题的 60% 以上 。核心在于理解 synchronized 的 JVM 实现与 AQS 的框架设计。
3.1 synchronized:JVM 层的 Monitor 机制
synchronized 是 Java 最基础的同步关键字,面试中几乎必问其底层实现与锁升级过程。
3.1.1 底层实现原理
synchronized 的锁载体是对象 。无论修饰实例方法、静态方法还是代码块,最终都绑定到一个对象(实例对象或 Class 对象)上。
对象在内存中的布局分为三部分:对象头(Header) 、实例数据(Instance Data) 、对齐填充(Padding) 。锁的信息存储在对象头的 Mark Word 中。
在 64 位 JVM 中,Mark Word 的锁状态布局如下:
| 锁状态 | 偏向锁位 | 锁标志位 | 存储内容 |
|---|---|---|---|
| 无锁 | 0 | 01 | hashCode(31bit) + 分代年龄 |
| 偏向锁 | 1 | 01 | 线程ID(54bit) + epoch(2bit) + 分代年龄 |
| 轻量级锁 | - | 00 | 指向栈中 Lock Record 的指针(62bit) |
| 重量级锁 | - | 10 | 指向堆中 Monitor 对象的指针(62bit) |
| GC标记 | - | 11 | 空(标记阶段专用) |
当进入 synchronized 代码块时,JVM 通过 monitorenter 指令尝试获取对象的 Monitor(监视器/管程);退出时通过 monitorexit 指令释放。Monitor 内部维护 _owner(持有线程)、_EntryList(阻塞队列)、_WaitSet(等待队列) 。
3.1.2 锁升级流程(面试核心)
JDK 6 之前,synchronized 直接关联操作系统 Mutex,线程阻塞和唤醒需要用户态与内核态切换,性能低下。JDK 6 引入了锁升级机制,根据竞争程度动态调整锁策略:
① 无锁状态
对象刚创建时,无竞争。Mark Word 存储对象的 hashCode 和分代年龄。
② 偏向锁(Biased Locking)
当单线程反复进入同步块时,JVM 通过 CAS 将 Mark Word 中的线程 ID 替换为当前线程 ID。后续该线程再次进入时,只需检查线程 ID 是否一致,无需任何 CAS 或内核调用,性能接近无锁。
面试追问:JDK 15 起默认禁用偏向锁,JDK 16 移除相关 API。原因是偏向锁的撤销(Revoke)在复杂应用中成本过高,且现代应用多为多线程竞争,偏向锁收益有限 。
③ 轻量级锁(Lightweight Locking)
当第二个线程 尝试获取锁时,偏向锁撤销。线程在自己的栈帧中创建 Lock Record (锁记录),通过 CAS 将对象头的 Mark Word 替换为指向 Lock Record 的指针。如果 CAS 成功,则获取锁;如果失败,线程自旋等待(不阻塞)。
自旋次数由 自适应自旋(Adaptive Spinning) 控制:JVM 根据历史成功率动态调整,成功率高则多自旋几次,反之少自旋 。
④ 重量级锁(Heavyweight Locking)
当多个线程同时竞争 (自旋失败),锁膨胀为重量级锁。对象头指向堆中的 ObjectMonitor,未获取到锁的线程调用 LockSupport.park 进入内核态阻塞,等待持有线程释放后由 unpark 唤醒。
关键结论 :锁升级是单向的(偏向→轻量→重量),但释放锁后轻量级/偏向锁可以恢复为无锁状态。
3.1.3 可重入性
synchronized 是可重入锁 。同一线程多次获取同一把锁时,Monitor 中的 _recursions 计数递增;退出时递减,归零才真正释放。这避免了同一线程内的死锁。
3.1.4 锁优化技术
- 锁消除(Lock Elimination) :JVM 逃逸分析确定对象不会被其他线程访问,自动去掉
synchronized。例如StringBuffer的局部变量拼接。 - 锁粗化(Lock Coarsening):相邻的同步块对同一对象加锁,JVM 合并为一个更大的同步块,减少加锁解锁开销。
- 自适应自旋:根据历史成功率动态调整自旋次数。
3.1.5 适用场景与框架应用
- 适用场景 :简单的线程安全保护,如计数器、单例模式(DCL 配合
volatile)。 - 框架应用 :
- JDK 早期线程安全类 :
Vector、Hashtable、StringBuffer内部大量使用synchronized,性能较差,已被ConcurrentHashMap、StringBuilder替代。 - Spring 单例 Bean:Spring 容器中的单例对象默认线程安全,但业务代码中若操作共享状态仍需加锁。
- JDK 早期线程安全类 :
3.2 volatile:轻量级的可见性与有序性保证
volatile 不是锁,但面试中常与锁一起考察,因为它解决了锁无法高效解决的可见性问题。
3.2.1 核心机制
- 可见性 :线程修改
volatile变量后,立即刷新到主内存;读取时直接从主内存拉取,绕过线程本地缓存。 - 禁止指令重排序 :通过插入内存屏障 实现。
StoreStore屏障:禁止普通写与volatile写重排序。StoreLoad屏障:volatile写之后插入,禁止后续读写重排序到前面。
3.2.2 适用场景与经典案例
-
状态标志位:控制线程循环退出。
-
DCL 单例模式 :防止对象半初始化(
instance = new Singleton()的三步:分配内存→初始化→赋值引用可能被重排序)。javaprivate volatile static Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); // volatile 禁止重排序 } } } return instance; }
面试陷阱 :
volatile不保证原子性 。volatile i++仍然非线程安全,因为i++是读取→加1→写入三步操作,需要配合锁或 CAS。
3.3 ReentrantLock & AQS:JUC 的基石
ReentrantLock 是 JDK 5 引入的显式锁,基于 AbstractQueuedSynchronizer(AQS) 实现,提供了比 synchronized 更丰富的控制能力。
3.3.1 ReentrantLock vs synchronized(面试必背)
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层 | JVM 层(Monitor) | API 层(JDK) |
| 锁获取 | 自动(进入/退出代码块) | 手动 lock() / unlock() |
| 可中断 | ❌ | ✅ lockInterruptibly() |
| 超时获取 | ❌ | ✅ tryLock(timeout, unit) |
| 公平锁 | ❌ 非公平 | ✅ 可配置(默认非公平) |
| 条件变量 | 一个(wait/notify) |
多个 Condition |
| 性能 | JDK 6+ 优化后接近 | 略高(CAS + 自旋) |
| 释放机制 | 自动(异常也释放) | 必须在 finally 中手动释放 |
3.3.2 AQS 核心原理(面试源码级考点)
AQS 是 JUC 包的底层骨架 ,ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock 均基于它实现。
三大核心组件:
volatile int state:同步状态。ReentrantLock中state=0未锁定,state>0表示重入次数。- FIFO 双向队列(CLH 变体) :获取锁失败的线程被封装为
Node节点入队,队列头为虚节点(不存储线程)。 - CAS 操作 :通过
Unsafe.compareAndSwapInt原子修改state,保证无锁线程安全。
独占模式获取锁流程:
java
// ReentrantLock.NonfairSync
final void lock() {
if (compareAndSetState(0, 1)) // CAS 抢锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 失败则进入 AQS 队列
}
// AQS.acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
线程入队后,在 acquireQueued 中自旋:若前驱节点是头节点,则尝试获取锁;否则判断是否需要 park 阻塞(通过 shouldParkAfterFailedAcquire 将前驱状态设为 SIGNAL)。
公平锁与非公平锁的源码差异:
java
// FairSync.tryAcquire
if (c == 0) {
if (!hasQueuedPredecessors() && // 公平锁多此判断:队列是否有前驱
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
非公平锁允许新线程直接 CAS 抢锁;公平锁必须检查队列中是否有等待更久的线程,保证 FIFO 。
3.3.3 Condition 条件队列
ReentrantLock 可创建多个 Condition(lock.newCondition()),实现精准唤醒 。对比 Object.wait/notify:
wait/notify只能随机唤醒一个或全部,无法区分条件。Condition.await/signal可针对不同条件(如"队列不满"、"队列不空")分别唤醒,避免惊群效应。
典型应用:ArrayBlockingQueue 使用两个 Condition(notFull、notEmpty)分别管理生产者与消费者。
3.3.4 适用场景与框架应用
- 适用场景 :需要超时、可中断、公平性、多条件变量的复杂同步场景。
- 框架应用 :
- AQS 全家桶 :
CountDownLatch(共享模式,state 为计数器)、Semaphore(共享模式,state 为许可证数)、CyclicBarrier(基于ReentrantLock+Condition)。 - Dubbo:部分并发控制逻辑参考 AQS 设计。
- AQS 全家桶 :
3.4 ReentrantReadWriteLock:读多写少的利器
将锁拆分为读锁(共享)和写锁(排他):
- 读锁:多个线程可同时持有,阻塞写锁。
- 写锁:独占,阻塞读写。
锁降级(Lock Downgrading) :写锁可以降级为读锁(获取写锁→获取读锁→释放写锁),支持数据修改后安全发布。锁升级(读→写)不被支持,会导致死锁。
适用场景:读多写少的缓存系统。例如:
java
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Map<String, Object> cache = new HashMap<>();
public Object get(String key) {
rwl.readLock().lock();
try { return cache.get(key); } finally { rwl.readLock().unlock(); }
}
public void put(String key, Object value) {
rwl.writeLock().lock();
try { cache.put(key, value); } finally { rwl.writeLock().unlock(); }
}
3.5 StampedLock:乐观读的性能极致
JDK 8 引入,解决 ReentrantReadWriteLock 的写锁饥饿问题(读锁长期占用,写锁无法进入)。
三种模式:
- 写锁 :独占,与
ReadWriteLock相同。 - 悲观读锁 :共享,与
ReadWriteLock相同。 - 乐观读(Optimistic Read) :不加锁,仅获取一个戳(Stamp),读取后验证戳是否变化。若未变化则读取有效;若变化则升级为悲观读锁。
适用场景:读极多、写极少,且读操作耗时较长的场景(如复杂对象图遍历)。
3.6 Semaphore / CountDownLatch / CyclicBarrier
三者均基于 AQS 共享模式:
| 工具 | 核心机制 | 适用场景 |
|---|---|---|
| Semaphore | 共享模式,state 为许可证数 | 限流(如数据库连接池、接口 QPS 控制) |
| CountDownLatch | 共享模式,state 为计数器 | 多线程协调(如主线程等待多个子线程完成) |
| CyclicBarrier | ReentrantLock + Condition |
分阶段并行计算(如分片数据处理,每阶段汇聚) |
CountDownLatch 计数器归零后不可复用;CyclicBarrier 可循环使用,且支持到达屏障时的回调动作。
3.7 无锁编程:CAS 与 Atomic 类
锁的代价在于线程阻塞与唤醒。若竞争不激烈,可通过 CAS(Compare-And-Swap) 实现无锁并发。
java
// AtomicInteger 的 CAS 操作
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
ABA 问题 :线程 A 读取值为 1,线程 B 改为 2 又改回 1,线程 A 的 CAS 仍能成功,但中间状态已变。解决方案:AtomicStampedReference,通过版本号区分。
框架应用:
- ConcurrentHashMap(JDK 1.8) :数组定位用
volatile,链表/红黑树头节点插入用synchronized(细粒度,只锁头节点),计数用CAS+LongAdder(baseCount+CounterCell数组分散热点)。 - LongAdder :高并发计数器,通过分段(
Cell数组)减少 CAS 冲突,性能远超AtomicLong。
第四章 MySQL 锁------InnoDB 并发控制精要
数据库锁是后端面试的另一大高地。InnoDB 的锁系统精密复杂,理解其设计思想是排查死锁、优化事务性能的前提。
4.1 粒度与引擎差异
| 引擎 | 锁粒度 | 特点 |
|---|---|---|
| MyISAM | 表锁 | 开销小、并发低;读锁共享、写锁排他 |
| InnoDB | 行锁 + 表锁 | 支持事务、高并发;行锁基于索引实现 |
面试陷阱 :InnoDB 的行锁是加在索引项上的,而非物理行。若 SQL 未命中索引,会退化为全表扫描,行锁实际升级为"表锁效果"(锁住所有行)。
4.2 行锁的三重算法(面试高频)
InnoDB 定义了三种行锁算法,在 可重复读(RR) 隔离级别下协同防止幻读 :
| 算法 | 锁定范围 | 核心作用 |
|---|---|---|
| 记录锁(Record Lock) | 单条索引记录 | 精确锁定 WHERE id = 1 的行 |
| 间隙锁(Gap Lock) | 索引记录之间的间隙 | 防止幻读,锁定 (10, 20) 区间,不包含端点 |
| 临键锁(Next-Key Lock) | 记录 + 前面间隙 | 左开右闭区间 (前记录, 当前记录],RR 默认行锁模式 |
插入意向锁(Insert Intention Lock) :一种特殊的间隙锁,表示事务"计划在某个间隙插入"。多个事务向同一间隙的不同位置插入时,插入意向锁互相兼容,可并发插入。这与普通间隙锁"完全阻塞区间内所有插入"形成对比,是 InnoDB 高并发写入的关键优化 。
RC vs RR 差异 :读已提交(RC)下,InnoDB 仅使用记录锁,无间隙锁,死锁概率更低,但无法防止幻读。
4.3 按功能分类
4.3.1 共享锁(S)与排他锁(X)
| 锁类型 | 加锁方式 | 适用场景 |
|---|---|---|
| 共享锁(S) | SELECT ... LOCK IN SHARE MODE |
读操作,防数据被修改 |
| 排他锁(X) | SELECT ... FOR UPDATE / UPDATE / DELETE |
写操作,独占数据(库存扣减) |
兼容性:S-S 兼容,S-X 冲突,X-X 冲突。
4.3.2 意向锁(IS/IX)
意向锁是表级锁,作为行锁的"预先声明":
- IS(意向共享):事务准备对某些行加 S 锁
- IX(意向排他):事务准备对某些行加 X 锁
设计意图 :当需要加表锁(如 LOCK TABLES)时,只需检查表上是否有冲突的意向锁,无需逐行扫描即可判断行锁冲突,将锁检查复杂度从 O(n) 降至 O(1) 。
4.3.3 自增锁(AUTO-INC Lock)
表级锁,插入自增列时获取。通过 innodb_autoinc_lock_mode 控制:
0:传统模式,所有插入加锁。1(默认):连续模式,批量插入加锁,单条插入轻量。2:交错模式,性能最高但自增值不连续。
4.4 乐观锁与悲观锁
| 策略 | 实现方式 | 适用场景 |
|---|---|---|
| 悲观锁 | SELECT ... FOR UPDATE |
冲突概率高(库存扣减、转账) |
| 乐观锁 | 版本号/时间戳 CAS | 冲突概率低(积分更新、点赞数) |
乐观锁示例:
sql
UPDATE product SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = #{currentVersion} AND stock > 0;
若更新行数为 0,说明数据被其他事务修改,业务层需重试或抛异常。
4.5 死锁:排查与预防
4.5.1 四大必要条件
- 互斥:锁同一时间只能被一个事务持有。
- 占有且等待:事务持有锁,又申请新锁,不释放已有锁。
- 不可剥夺:锁只能由持有者主动释放。
- 循环等待:事务间形成头尾相接的等待环。
破坏任意一个条件即可避免死锁。
4.5.2 排查命令
- MySQL 8.0+ :
performance_schema.data_locks/data_lock_waits - 通用 :
SHOW ENGINE INNODB STATUS;(查看LATEST DETECTED DEADLOCK) - 日志 :开启
innodb_print_all_deadlocks=ON,死锁信息自动写入错误日志 。
4.5.3 高频死锁场景
- 更新顺序相反(最常见):事务 A 先更新 id=1 再更新 id=2,事务 B 先更新 id=2 再更新 id=1。
- 间隙锁 + 插入意向锁冲突(RR 高频):两事务先对同一间隙加间隙锁,再同时插入,互相等待。
- 无索引退化成表锁:更新未命中索引,锁全表,多事务并发更新不同行形成循环等待 。
4.5.4 预防策略
- 事务小而快,不拿着锁做无关操作。
- 固定加锁顺序(如先锁用户表,再锁订单表)。
- 读已提交(RC)隔离级别无间隙锁,死锁更少。
- 必加合理索引,避免锁全表。
- 高并发优先用乐观锁。
4.6 框架应用
- Spring @Transactional :事务隔离级别(
Isolation.READ_COMMITTED等)直接决定 InnoDB 的锁行为。RC 无间隙锁,RR 有间隙锁。 - MyBatis-Plus 乐观锁 :
@Version注解自动在更新时追加version = version + 1条件。 - ShardingSphere:分布式事务(Seata AT 模式)中,本地事务的锁由 InnoDB 管理,全局事务由 TC 协调。
第五章 分布式锁------跨 JVM 的互斥契约
单机锁无法解决微服务/集群环境下的资源竞争。分布式锁的本质是在 CAP 定理 下做权衡:
- AP 型:优先可用性(Redis)
- CP 型:优先一致性(ZooKeeper / etcd)
5.1 设计维度
一个合格的分布式锁必须满足:
- 互斥性:同一时刻只有一个客户端持有锁。
- 防死锁:客户端崩溃后锁能自动释放。
- 可重入性:同一线程/客户端可多次获取锁。
- 自动续约:业务执行时间长于锁过期时间时自动续期。
- 一致性:主从切换或网络分区时,锁不丢失。
5.2 Redis 分布式锁:性能至上的 AP 方案
5.2.1 基础命令
bash
SET lock_key unique_client_id NX PX 30000
NX:仅当 key 不存在时才设置(保证互斥)。PX 30000:30 秒过期(防死锁)。unique_client_id:唯一标识(防误删)。
解锁必须使用 Lua 脚本保证原子性:
lua
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
5.2.2 三大缺陷
- 主从切换锁丢失:主节点写入锁后尚未同步到从节点即宕机,从节点晋升为主节点后锁信息丢失。
- 业务超时锁过期:业务执行时间超过过期时间,锁提前释放,其他线程可获取锁,导致并发安全问题。
- 误删他人锁:解锁时未校验归属,可能删除其他客户端的锁。
5.2.3 Redisson:工业级实现(面试源码级考点)
Redisson 是 Redis 分布式锁的业界标准,解决了上述所有缺陷 :
① 可重入锁实现
基于 Hash 结构存储锁状态:
- Key:锁名
- Field:
UUID:threadId - Value:重入计数
加锁/解锁通过 Lua 脚本 原子执行,重入时递增计数,解锁时递减,计数归零才真正删除。
② WatchDog 看门狗自动续期
- 默认锁过期时间 30 秒。
- 客户端加锁成功后,后台启动看门狗线程,每 10 秒 自动将过期时间重置为 30 秒。
- 只要客户端存活,锁就持续有效;客户端崩溃后看门狗停止,锁在 30 秒后自动释放,防死锁。
③ 公平锁与非公平锁
- 非公平锁(默认):锁释放后多客户端直接 CAS 抢锁,吞吐高但可能饥饿。
- 公平锁:按请求顺序排队获取,通过 Redis List 维护 FIFO 队列,延迟可控但吞吐较低。
④ 丰富的锁类型
联锁(MultiLock)、红锁(RedLock)、读写锁等全场景支持。
5.2.4 Redlock 红锁算法
Redis 作者提出的多主节点方案:
- 部署 N 个独立 Redis 主节点(推荐 N=5)。
- 向所有节点顺序发起加锁,每个请求设置极短超时(如 5ms)。
- 成功加锁节点数 K ≥ N/2 + 1,且总耗时 < 锁过期时间,则加锁成功。
- 解锁时向所有节点发送 Lua 脚本。
争议点 :Redlock 依赖各节点的本地时钟,时钟回拨会导致锁提前过期。分布式系统专家 Martin Kleppmann 指出其在异步网络模型下的安全性缺陷,使其成为最具争议的分布式锁算法之一 。
5.2.5 适用场景与框架应用
- 适用场景:高并发、非核心业务(秒杀库存扣减、限流、缓存击穿防护)。
- 框架应用 :
- Spring Boot + Redisson :
@Bean RedissonClient,通过RLock操作。 - 秒杀系统:Redisson 锁保护库存扣减,配合 Lua 脚本实现 Redis 原子预扣。
- 缓存击穿防护:热点 Key 过期时,仅一个线程回源数据库。
- Spring Boot + Redisson :
5.3 ZooKeeper 分布式锁:天然公平的 CP 方案
5.3.1 核心原理
利用 ZooKeeper 的三大特性 :
- 临时节点(Ephemeral):客户端断开自动删除,天然防死锁。
- 有序节点(Sequential):自动分配全局递增序号,天然实现公平排队。
- Watcher 机制:监听前一个节点的删除事件,避免轮询。
公平锁实现流程:
- 在
/distributed_lock下创建临时有序节点(如lock-0000000001)。 - 判断自己的序号是否为最小:是则获取锁。
- 否则监听前一个序号节点的删除事件,阻塞等待。
- 锁释放时,临时节点自动删除,后续节点被唤醒。
5.3.2 Curator 工业级实现
Apache Curator 的 InterProcessMutex 是标准实现,封装了上述流程,并优化了羊群效应、会话超时、可重入性等问题。
5.3.3 优缺点
| 优势 | 劣势 |
|---|---|
| CP 模型,ZAB 协议保证强一致性 | 性能中等,QPS 仅万级 |
| 天然防死锁、天然公平 | 运维成本高,需独立维护 ZK 集群 |
| Watcher 实时通知,无惊群效应 | 会话超时风险:GC 停顿导致心跳中断,锁意外释放 |
| 完全不依赖本地时钟 | 不适合短生命周期锁场景 |
5.3.4 适用场景与框架应用
- 适用场景:强一致性、低并发场景(分布式任务调度、金融交易、数据同步)。
- 框架应用 :
- Dubbo:早期注册中心使用 ZK,服务上下线通过临时节点实现。
- Elastic-Job:分布式任务调度,通过 ZK 锁实现分片任务的互斥执行。
5.4 etcd 分布式锁:云原生时代的 CP 选择
5.4.1 核心原理
etcd 基于 Raft 共识算法,是 Kubernetes 的默认协调服务 :
- Lease 租约机制 :客户端创建 TTL 租约并绑定锁 key,通过
KeepAlive心跳自动续约;客户端崩溃后租约过期,key 自动删除。 - 全局 Revision:每次写操作生成全局递增版本号,天然实现公平排序。
- 精准 Watch:监听前一个 Revision 的 key,无羊群效应。
5.4.2 官方 concurrency 包
etcd 提供 concurrency.Mutex,开箱即用:
go
s, err := concurrency.NewSession(client, concurrency.WithTTL(10))
mu := concurrency.NewMutex(s, "my-lock/")
mu.Lock(context.TODO())
// 业务逻辑
mu.Unlock(context.TODO())
5.4.3 优缺点
| 优势 | 劣势 |
|---|---|
| Raft 强线性一致性,无锁丢失 | 性能弱于 Redis,QPS 万级 |
| 租约由集群统一管理,无时钟回拨风险 | 运维有门槛,适合已有 etcd 集群 |
| Watch 支持断点续传,无 ZK 的 Watcher 一次性失效问题 | 生态成熟度弱于 Redis/ZK |
5.4.4 适用场景与框架应用
- 适用场景:云原生/Kubernetes 生态,兼顾一致性与可靠性的场景。
- 框架应用 :
- Kubernetes :Controller 通过 etcd 选举(
Lease+Endpoint)实现 Leader 选举。 - 分布式配置中心:如 Apollo 可通过 etcd 实现配置发布的分布式锁。
- Kubernetes :Controller 通过 etcd 选举(
5.5 数据库分布式锁
基于数据库的唯一索引或乐观锁实现:
- 唯一索引插入 :
INSERT INTO lock_table (resource) VALUES ('order_1'),利用唯一约束互斥。释放时删除记录。 - 版本号 CAS :
UPDATE lock_table SET version = version + 1 WHERE resource = 'order_1' AND version = 0。
缺点:无阻塞等待机制(需轮询或业务层重试),性能差,不适合高并发。
5.6 方案对比与选型(面试必背)
| 维度 | Redis (Redisson) | ZooKeeper (Curator) | etcd |
|---|---|---|---|
| 一致性 | 最终一致性(AP) | 线性一致性(CP) | 线性一致性(CP) |
| 性能 | 极高(10万+ QPS) | 中等(万级 QPS) | 中等(万级 QPS) |
| 死锁防护 | WatchDog 过期 + Lua | 临时节点自动删除 | Lease 租约过期 |
| 可重入 | ✅(Hash 计数) | ✅(ThreadLocal) | ✅(concurrency包) |
| 公平性 | 默认非公平 | 天然公平 | 天然公平 |
| 时钟回拨 | 致命风险 | 无影响 | 无影响 |
| 适用场景 | 秒杀、限流、缓存 | 金融交易、任务调度 | K8s 云原生系统 |
第六章 锁的优化与最佳实践
6.1 减少锁粒度
将大锁拆分为小锁。经典案例:
- ConcurrentHashMap(JDK 1.8):数组每个桶(链表头节点)独立加锁,而非全局锁。高并发下不同桶的操作互不阻塞。
- LongAdder :将计数拆分为
base+Cell[]数组,线程哈希到不同 Cell 上 CAS,最后求和。
6.2 锁分离
- 读写分离 :
ReentrantReadWriteLock、StampedLock将读与写分离,读读不互斥。 - 锁分段 :
LinkedBlockingQueue使用takeLock和putLock分别管理取与放,读写分离。
6.3 锁消除与锁粗化(JVM 层)
- 锁消除 :逃逸分析证明对象不会被其他线程访问,去掉
synchronized。 - 锁粗化:相邻同步块合并,减少加锁解锁次数。
6.4 无锁化替代
- ThreadLocal :线程私有变量,彻底消除竞争。Spring 的
RequestContextHolder、日志的MDC均基于此。 - CAS + Atomic :低竞争计数器优先使用
LongAdder而非synchronized。 - Disruptor:通过环形队列和内存屏障实现无锁高并发队列。
6.5 避免死锁的五个原则
- 固定加锁顺序:所有线程按相同顺序获取锁。
- 缩短锁持有时间:不做 IO、RPC 等耗时操作。
- 使用超时机制 :
tryLock(timeout)替代无限等待。 - 降低隔离级别:RC 替代 RR,消除间隙锁。
- 优先乐观锁:低冲突场景用版本号替代数据库悲观锁。
第七章 面试高频真题与深度追问
以下是锁相关面试中命中率最高的问题及回答要点:
Java 并发锁(高频)
-
synchronized 的底层实现是什么?
- 答:JVM 层通过
monitorenter/monitorexit指令实现,依赖对象头的 Mark Word 和 Monitor 对象。
- 答:JVM 层通过
-
synchronized 锁升级过程?
- 答:无锁 → 偏向锁(单线程 CAS 替换线程 ID)→ 轻量级锁(栈帧 Lock Record + 自旋)→ 重量级锁(Monitor + 内核态阻塞)。JDK 15 后偏向锁默认禁用。
-
ReentrantLock 与 synchronized 的区别?
- 答:7 维度对比(实现层、自动/手动、可中断、超时、公平锁、多 Condition、性能)。
-
AQS 的核心原理?
- 答:
volatile int state+ CLH 变体 FIFO 队列 + CAS。独占模式(ReentrantLock)与共享模式(Semaphore)的区别在于tryAcquire返回值和唤醒传播机制。
- 答:
-
公平锁与非公平锁的源码差异?
- 答:公平锁在 CAS 前调用
hasQueuedPredecessors()检查队列是否有前驱线程,非公平锁直接 CAS 抢锁。
- 答:公平锁在 CAS 前调用
-
volatile 能保证线程安全吗?
- 答:能保证可见性和有序性,但不能保证原子性。
volatile i++仍非线程安全。
- 答:能保证可见性和有序性,但不能保证原子性。
-
什么是 CAS?ABA 问题如何解决?
- 答:Compare-And-Swap,原子指令。ABA 问题通过
AtomicStampedReference(版本号)解决。
- 答:Compare-And-Swap,原子指令。ABA 问题通过
MySQL 锁(高频)
-
InnoDB 的行锁是如何实现的?
- 答:加在索引项上。无索引时退化为全表扫描,锁住所有行。
-
Record Lock、Gap Lock、Next-Key Lock 的区别?
- 答:记录锁锁单行;间隙锁锁范围不含记录,防幻读;临键锁=记录锁+间隙锁,左开右闭,RR 默认。
-
意向锁的作用是什么?
- 答:表级锁,表示事务即将对行加锁。加表锁时只需检查意向锁冲突,无需逐行扫描。
-
RR 和 RC 隔离级别下锁的行为差异?
- 答:RR 有间隙锁和临键锁,防幻读但易死锁;RC 只有记录锁,无间隙锁。
-
如何排查 MySQL 死锁?
- 答:
SHOW ENGINE INNODB STATUS看LATEST DETECTED DEADLOCK;MySQL 8.0 查performance_schema.data_locks。
- 答:
-
乐观锁与悲观锁的适用场景?
- 答:悲观锁用于冲突高的库存扣减;乐观锁用于冲突低的积分更新,通过版本号 CAS。
分布式锁(高频)
-
Redis 分布式锁如何实现可重入?
- 答:Redisson 用 Hash 结构存储
clientId:threadId和重入计数,Lua 脚本原子操作。
- 答:Redisson 用 Hash 结构存储
-
WatchDog 看门狗机制是什么?
- 答:Redisson 默认 30 秒过期,每 10 秒自动续期。客户端存活则锁持续有效,崩溃后 30 秒自动释放。
-
Redis 分布式锁的主从切换问题如何解决?
- 答:Redlock 多主多数派,但仍有时钟回拨争议;金融级场景建议用 ZooKeeper/etcd。
-
ZooKeeper 分布式锁为什么天然公平?
- 答:临时有序节点按创建顺序分配序号,最小序号获取锁,后续节点监听前一个节点。
-
etcd 分布式锁相比 ZK 的优势?
- 答:Lease 租约由集群统一管理,无时钟回拨;Watch 支持断点续传,无会话过期和 Watcher 一次性失效问题。
-
分布式锁的选型依据?
- 答:高并发非核心选 Redis(AP);强一致性核心交易选 ZooKeeper/etcd(CP);避免时钟回拨选 etcd。
-
什么是锁降级?什么是锁升级?
- 答:
ReentrantReadWriteLock支持写锁降级为读锁(安全发布);不支持读锁升级为写锁(会导致死锁)。
- 答:
第八章 总结
锁是并发系统的"交通规则",没有它,多线程/多进程就是混乱的十字路口。本文从三个层次构建了完整的锁知识体系:
- Java 语言级 :
synchronized的锁升级是 JVM 优化的典范,AQS的state + FIFO 队列 + CAS是 JUC 的骨架,ReentrantLock的公平性与多Condition提供了更精细的控制。 - 数据库级:InnoDB 的行锁基于索引,RR 下的临键锁构筑了幻读防线,意向锁是表锁与行锁协调的高效桥梁。
- 分布式级:Redis(AP)追求极致性能,ZooKeeper/etcd(CP)追求强一致性。Redisson 的看门狗、Curator 的临时有序节点、etcd 的 Lease 租约,分别代表了三种工业级实现范式。
面试的最终建议 :当被问到锁相关问题时,先明确作用域(单机/分布式) 、策略(乐观/悲观) 、特性(公平/可重入/共享),再深入具体实现原理。展现出"分类清晰、原理透彻、场景明确"的思维框架,锁相关的面试题将不再是障碍。