锁:从操作系统到分布式系统的完整面试指南

锁是并发系统的灵魂,也是技术面试中永不落幕的核心考点。本文从并发问题的本质出发,系统梳理 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/进程内部,如 synchronizedReentrantLock
  • 分布式锁(跨进程锁) :作用于多个 JVM/机器之间,如 Redis RedissonZooKeeper Curator

按策略:

  • 悲观锁:先加锁,再操作。认为冲突必然发生。
  • 乐观锁:不加锁,提交时检查版本号。认为冲突很少发生。

按特性:

  • 公平锁:按请求顺序分配锁(FIFO)。
  • 非公平锁:允许"插队",吞吐更高。
  • 可重入锁 :同一线程可多次获取同一把锁(synchronizedReentrantLock)。
  • 不可重入锁:同一线程再次获取会死锁。
  • 共享锁(读锁):多个线程可同时持有。
  • 排他锁(写锁):仅一个线程可持有。

按实现层次:

  • 语言级 :Java synchronizedReentrantLockStampedLock
  • 操作系统级MutexSpinlockSemaphore
  • 数据库级InnoDB 行锁、表锁、间隙锁
  • 分布式协调级RedisZooKeeperetcd

面试考点地图:

  • 初级synchronized 用法、volatile 作用、乐观锁悲观锁区别
  • 中级 :锁升级过程、ReentrantLocksynchronized 区别、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 早期线程安全类VectorHashtableStringBuffer 内部大量使用 synchronized,性能较差,已被 ConcurrentHashMapStringBuilder 替代。
    • Spring 单例 Bean:Spring 容器中的单例对象默认线程安全,但业务代码中若操作共享状态仍需加锁。

3.2 volatile:轻量级的可见性与有序性保证

volatile 不是锁,但面试中常与锁一起考察,因为它解决了锁无法高效解决的可见性问题。

3.2.1 核心机制
  • 可见性 :线程修改 volatile 变量后,立即刷新到主内存;读取时直接从主内存拉取,绕过线程本地缓存。
  • 禁止指令重排序 :通过插入内存屏障 实现。
    • StoreStore 屏障:禁止普通写与 volatile 写重排序。
    • StoreLoad 屏障:volatile 写之后插入,禁止后续读写重排序到前面。
3.2.2 适用场景与经典案例
  • 状态标志位:控制线程循环退出。

  • DCL 单例模式 :防止对象半初始化(instance = new Singleton() 的三步:分配内存→初始化→赋值引用可能被重排序)。

    java 复制代码
    private 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 包的底层骨架ReentrantLockCountDownLatchSemaphoreReentrantReadWriteLock 均基于它实现。

三大核心组件:

  1. volatile int state :同步状态。ReentrantLockstate=0 未锁定,state>0 表示重入次数。
  2. FIFO 双向队列(CLH 变体) :获取锁失败的线程被封装为 Node 节点入队,队列头为虚节点(不存储线程)。
  3. 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 可创建多个 Conditionlock.newCondition()),实现精准唤醒 。对比 Object.wait/notify

  • wait/notify 只能随机唤醒一个或全部,无法区分条件。
  • Condition.await/signal 可针对不同条件(如"队列不满"、"队列不空")分别唤醒,避免惊群效应

典型应用:ArrayBlockingQueue 使用两个 ConditionnotFullnotEmpty)分别管理生产者与消费者。

3.3.4 适用场景与框架应用
  • 适用场景 :需要超时、可中断、公平性、多条件变量的复杂同步场景。
  • 框架应用
    • AQS 全家桶CountDownLatch(共享模式,state 为计数器)、Semaphore(共享模式,state 为许可证数)、CyclicBarrier(基于 ReentrantLock + Condition)。
    • Dubbo:部分并发控制逻辑参考 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 + LongAdderbaseCount + 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 四大必要条件
  1. 互斥:锁同一时间只能被一个事务持有。
  2. 占有且等待:事务持有锁,又申请新锁,不释放已有锁。
  3. 不可剥夺:锁只能由持有者主动释放。
  4. 循环等待:事务间形成头尾相接的等待环。

破坏任意一个条件即可避免死锁。

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 高频死锁场景
  1. 更新顺序相反(最常见):事务 A 先更新 id=1 再更新 id=2,事务 B 先更新 id=2 再更新 id=1。
  2. 间隙锁 + 插入意向锁冲突(RR 高频):两事务先对同一间隙加间隙锁,再同时插入,互相等待。
  3. 无索引退化成表锁:更新未命中索引,锁全表,多事务并发更新不同行形成循环等待 。
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 设计维度

一个合格的分布式锁必须满足:

  1. 互斥性:同一时刻只有一个客户端持有锁。
  2. 防死锁:客户端崩溃后锁能自动释放。
  3. 可重入性:同一线程/客户端可多次获取锁。
  4. 自动续约:业务执行时间长于锁过期时间时自动续期。
  5. 一致性:主从切换或网络分区时,锁不丢失。

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 三大缺陷
  1. 主从切换锁丢失:主节点写入锁后尚未同步到从节点即宕机,从节点晋升为主节点后锁信息丢失。
  2. 业务超时锁过期:业务执行时间超过过期时间,锁提前释放,其他线程可获取锁,导致并发安全问题。
  3. 误删他人锁:解锁时未校验归属,可能删除其他客户端的锁。
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 作者提出的多主节点方案:

  1. 部署 N 个独立 Redis 主节点(推荐 N=5)。
  2. 向所有节点顺序发起加锁,每个请求设置极短超时(如 5ms)。
  3. 成功加锁节点数 K ≥ N/2 + 1,且总耗时 < 锁过期时间,则加锁成功。
  4. 解锁时向所有节点发送 Lua 脚本。

争议点 :Redlock 依赖各节点的本地时钟,时钟回拨会导致锁提前过期。分布式系统专家 Martin Kleppmann 指出其在异步网络模型下的安全性缺陷,使其成为最具争议的分布式锁算法之一 。

5.2.5 适用场景与框架应用
  • 适用场景:高并发、非核心业务(秒杀库存扣减、限流、缓存击穿防护)。
  • 框架应用
    • Spring Boot + Redisson@Bean RedissonClient,通过 RLock 操作。
    • 秒杀系统:Redisson 锁保护库存扣减,配合 Lua 脚本实现 Redis 原子预扣。
    • 缓存击穿防护:热点 Key 过期时,仅一个线程回源数据库。

5.3 ZooKeeper 分布式锁:天然公平的 CP 方案

5.3.1 核心原理

利用 ZooKeeper 的三大特性 :

  • 临时节点(Ephemeral):客户端断开自动删除,天然防死锁。
  • 有序节点(Sequential):自动分配全局递增序号,天然实现公平排队。
  • Watcher 机制:监听前一个节点的删除事件,避免轮询。

公平锁实现流程

  1. /distributed_lock 下创建临时有序节点(如 lock-0000000001)。
  2. 判断自己的序号是否为最小:是则获取锁。
  3. 否则监听前一个序号节点的删除事件,阻塞等待。
  4. 锁释放时,临时节点自动删除,后续节点被唤醒。
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 的默认协调服务 :

  1. Lease 租约机制 :客户端创建 TTL 租约并绑定锁 key,通过 KeepAlive 心跳自动续约;客户端崩溃后租约过期,key 自动删除。
  2. 全局 Revision:每次写操作生成全局递增版本号,天然实现公平排序。
  3. 精准 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 实现配置发布的分布式锁。

5.5 数据库分布式锁

基于数据库的唯一索引或乐观锁实现:

  • 唯一索引插入INSERT INTO lock_table (resource) VALUES ('order_1'),利用唯一约束互斥。释放时删除记录。
  • 版本号 CASUPDATE 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 锁分离

  • 读写分离ReentrantReadWriteLockStampedLock 将读与写分离,读读不互斥。
  • 锁分段LinkedBlockingQueue 使用 takeLockputLock 分别管理取与放,读写分离。

6.3 锁消除与锁粗化(JVM 层)

  • 锁消除 :逃逸分析证明对象不会被其他线程访问,去掉 synchronized
  • 锁粗化:相邻同步块合并,减少加锁解锁次数。

6.4 无锁化替代

  • ThreadLocal :线程私有变量,彻底消除竞争。Spring 的 RequestContextHolder、日志的 MDC 均基于此。
  • CAS + Atomic :低竞争计数器优先使用 LongAdder 而非 synchronized
  • Disruptor:通过环形队列和内存屏障实现无锁高并发队列。

6.5 避免死锁的五个原则

  1. 固定加锁顺序:所有线程按相同顺序获取锁。
  2. 缩短锁持有时间:不做 IO、RPC 等耗时操作。
  3. 使用超时机制tryLock(timeout) 替代无限等待。
  4. 降低隔离级别:RC 替代 RR,消除间隙锁。
  5. 优先乐观锁:低冲突场景用版本号替代数据库悲观锁。

第七章 面试高频真题与深度追问

以下是锁相关面试中命中率最高的问题及回答要点:

Java 并发锁(高频)

  1. synchronized 的底层实现是什么?

    • 答:JVM 层通过 monitorenter/monitorexit 指令实现,依赖对象头的 Mark Word 和 Monitor 对象。
  2. synchronized 锁升级过程?

    • 答:无锁 → 偏向锁(单线程 CAS 替换线程 ID)→ 轻量级锁(栈帧 Lock Record + 自旋)→ 重量级锁(Monitor + 内核态阻塞)。JDK 15 后偏向锁默认禁用。
  3. ReentrantLock 与 synchronized 的区别?

    • 答:7 维度对比(实现层、自动/手动、可中断、超时、公平锁、多 Condition、性能)。
  4. AQS 的核心原理?

    • 答:volatile int state + CLH 变体 FIFO 队列 + CAS。独占模式(ReentrantLock)与共享模式(Semaphore)的区别在于 tryAcquire 返回值和唤醒传播机制。
  5. 公平锁与非公平锁的源码差异?

    • 答:公平锁在 CAS 前调用 hasQueuedPredecessors() 检查队列是否有前驱线程,非公平锁直接 CAS 抢锁。
  6. volatile 能保证线程安全吗?

    • 答:能保证可见性和有序性,但不能保证原子性。volatile i++ 仍非线程安全。
  7. 什么是 CAS?ABA 问题如何解决?

    • 答:Compare-And-Swap,原子指令。ABA 问题通过 AtomicStampedReference(版本号)解决。

MySQL 锁(高频)

  1. InnoDB 的行锁是如何实现的?

    • 答:加在索引项上。无索引时退化为全表扫描,锁住所有行。
  2. Record Lock、Gap Lock、Next-Key Lock 的区别?

    • 答:记录锁锁单行;间隙锁锁范围不含记录,防幻读;临键锁=记录锁+间隙锁,左开右闭,RR 默认。
  3. 意向锁的作用是什么?

    • 答:表级锁,表示事务即将对行加锁。加表锁时只需检查意向锁冲突,无需逐行扫描。
  4. RR 和 RC 隔离级别下锁的行为差异?

    • 答:RR 有间隙锁和临键锁,防幻读但易死锁;RC 只有记录锁,无间隙锁。
  5. 如何排查 MySQL 死锁?

    • 答:SHOW ENGINE INNODB STATUSLATEST DETECTED DEADLOCK;MySQL 8.0 查 performance_schema.data_locks
  6. 乐观锁与悲观锁的适用场景?

    • 答:悲观锁用于冲突高的库存扣减;乐观锁用于冲突低的积分更新,通过版本号 CAS。

分布式锁(高频)

  1. Redis 分布式锁如何实现可重入?

    • 答:Redisson 用 Hash 结构存储 clientId:threadId 和重入计数,Lua 脚本原子操作。
  2. WatchDog 看门狗机制是什么?

    • 答:Redisson 默认 30 秒过期,每 10 秒自动续期。客户端存活则锁持续有效,崩溃后 30 秒自动释放。
  3. Redis 分布式锁的主从切换问题如何解决?

    • 答:Redlock 多主多数派,但仍有时钟回拨争议;金融级场景建议用 ZooKeeper/etcd。
  4. ZooKeeper 分布式锁为什么天然公平?

    • 答:临时有序节点按创建顺序分配序号,最小序号获取锁,后续节点监听前一个节点。
  5. etcd 分布式锁相比 ZK 的优势?

    • 答:Lease 租约由集群统一管理,无时钟回拨;Watch 支持断点续传,无会话过期和 Watcher 一次性失效问题。
  6. 分布式锁的选型依据?

    • 答:高并发非核心选 Redis(AP);强一致性核心交易选 ZooKeeper/etcd(CP);避免时钟回拨选 etcd。
  7. 什么是锁降级?什么是锁升级?

    • 答:ReentrantReadWriteLock 支持写锁降级为读锁(安全发布);不支持读锁升级为写锁(会导致死锁)。

第八章 总结

锁是并发系统的"交通规则",没有它,多线程/多进程就是混乱的十字路口。本文从三个层次构建了完整的锁知识体系:

  • Java 语言级synchronized 的锁升级是 JVM 优化的典范,AQSstate + FIFO 队列 + CAS 是 JUC 的骨架,ReentrantLock 的公平性与多 Condition 提供了更精细的控制。
  • 数据库级:InnoDB 的行锁基于索引,RR 下的临键锁构筑了幻读防线,意向锁是表锁与行锁协调的高效桥梁。
  • 分布式级:Redis(AP)追求极致性能,ZooKeeper/etcd(CP)追求强一致性。Redisson 的看门狗、Curator 的临时有序节点、etcd 的 Lease 租约,分别代表了三种工业级实现范式。

面试的最终建议 :当被问到锁相关问题时,先明确作用域(单机/分布式)策略(乐观/悲观)特性(公平/可重入/共享),再深入具体实现原理。展现出"分类清晰、原理透彻、场景明确"的思维框架,锁相关的面试题将不再是障碍。

相关推荐
码农-阿杰20 小时前
深入理解 synchronized 底层实现:从 HotSpot C++ 源码看对象锁与 Monitor 机制
开发语言·c++·
如来神掌十八式1 个月前
Java所有的锁:从基础到进阶
java·
庞轩px1 个月前
深入理解 sleep() 与 wait():从基础到监视器队列
java·开发语言·线程··wait·sleep·监视器
敲代码的嘎仔2 个月前
Java后端开发——多线程面试题
java·开发语言·面试·多线程·八股·threadlocal·
庞轩px2 个月前
Synchronized 与 ReentrantLock 深度对比
并发编程·synchronized·aqs··reentrantlock
C++chaofan2 个月前
RPC框架负载均衡机制深度解析
java·开发语言·负载均衡·juc·synchronized·
Thomas.Sir2 个月前
深入剖析 Redis 经典面试题
redis·分布式·高并发·
我真会写代码2 个月前
深度解析并发编程锁升级:从偏向锁到重量级锁,底层原理+面试考点全拆解
java·并发编程·
C++chaofan2 个月前
JUC 并发编程:对可见性、有序性与 volatile的理解
java·开发语言·spring·java-ee·juc·synchronized·