面试必看:排队自旋锁之MCS锁的实现原理与关键考点 电子说
在并发编程面试中,“锁” 是绕不开的核心话题,而自旋锁作为轻量级锁的代表,其优化方案更是高频考点。其中,MCS 锁(以发明者 John Mellor-Crummey 和 Michael Scott 命名)作为排队自旋锁的经典实现,完美解决了传统自旋锁 “CPU 资源浪费”“缓存风暴” 等痛点,成为面试官评估候选人并发底层能力的重要标尺。今天,我们就从面试视角拆解 MCS 锁的实现逻辑,帮你轻松应对相关提问。
在讲 MCS 锁之前,我们得先明确 “传统自旋锁的问题”—— 这是面试中回答 “MCS 锁设计初衷” 的关键切入点。
传统自旋锁(如基于 CAS 的 Test-and-Set 锁)的核心问题的是 “盲等”:当多个线程竞争锁时,所有线程都会自旋等待同一个共享变量(如“锁状态标记”),哪怕锁已经被释放,也可能有大量线程无效自旋;更严重的是,多个线程频繁读取同一个变量会引发“缓存一致性风暴”(多个 CPU 核心缓存同步该变量,导致性能损耗)。
而 MCS 锁的核心思想是 “排队自旋”:让竞争锁的线程按顺序排成一个单向链表,每个线程只自旋等待 “前一个线程释放锁的信号”,避免对同一个共享变量的集中竞争。这种设计既减少了无效自旋,又消除了缓存风暴,是高并发场景下的最优自旋锁方案之一。
面试中,“MCS 锁的实现” 通常需要你讲清数据结构定义和 **“加锁、解锁、自旋” 三个核心流程 **。我们以 Java 为例(C/C++ 实现逻辑类似,只是语法差异),一步步拆解。
MCS 锁的核心是 “用链表记录等待线程”,每个线程对应一个Node节点,节点中需包含两个关键信息:
•isLocked:标记当前线程是否持有锁(或是否需要自旋);
•next:指向链表中的下一个等待线程(形成排队顺序)。
同时,锁本身需要一个共享的“尾指针”(tail),用于将新竞争锁的线程追加到链表尾部,这个尾指针通过 CAS 操作保证原子性。
Java 中的简化定义如下(面试时写出这个结构,基本就成功了一半):
// 1. 线程节点类:记录当前线程的锁状态和下一个等待线程class Node {// 标记是否需要自旋:true=需要等待,false=可获取锁volatile boolean isLocked = true;// 指向链表中的下一个节点(下一个等待线程)volatile Node next = null;}// 2. MCS锁类:核心是共享的尾指针tailpublic class MCSLock {// 共享尾指针:指向链表最后一个节点,初始为nullprivate volatile Node tail = null;// 每个线程独有的节点(避免线程安全问题,用ThreadLocal存储)private ThreadLocalcurrentNode = ThreadLocal.withInitial(Node::new); }
这里有个面试高频细节:为什么用 ThreadLocal 存储当前线程的 Node?
答:因为每个线程只能操作自己的 Node 节点(修改isLocked)和前一个线程的 Node 节点(设置next),用 ThreadLocal 可以避免多个线程竞争同一个 Node,同时保证每个线程对应唯一节点。
加锁的核心是“将当前线程追加到链表尾部,并找到前一个线程”,具体分 4 步(面试时建议结合步骤 + 代码 + 流程图讲解):

通过ThreadLocal拿到当前线程独有的Node,确保线程安全。
用 CAS 操作(compareAndSet)将tail从“旧值” 更新为 “当前节点”:
•如果 CAS 成功:说明当前线程是第一个竞争锁的线程(链表为空),直接获取锁,无需自旋;
•如果 CAS 失败:说明已有其他线程在排队,当前线程需要加入链表尾部。
CAS 失败后,旧的tail就是“前一个线程的节点(prev)”,需要将prev的next指向当前节点(把当前线程接入链表)。
当前线程自旋等待prev.isLocked变为false(前一个线程释放锁的信号),直到条件满足后,才能获取锁。
加锁的 Java 实现代码如下(面试时写出关键逻辑即可):
public void lock() {// 步骤1:获取当前线程的Node节点Node currNode = currentNode.get();// 步骤2:CAS尝试将当前节点设为新tail(入队)Node prevNode = CASUpdateTail(null, currNode) ? null : tail;// 步骤3:如果有前一个线程(prevNode不为null),则将当前节点接入链表if (prevNode != null) {// 将前一个节点的next指向当前节点(当前线程排队到prev后面)prevNode.next = currNode;// 步骤4:自旋等待前一个线程释放锁(直到prev.isLocked为false)while (currNode.isLocked) {// 自旋:可加Thread.yield()减少CPU占用(面试可提优化点)Thread.yield();}}// 如果prevNode为null(CAS成功),直接获取锁,无需自旋}// 辅助方法:CAS更新tail指针(简化版,实际需用Unsafe类或AtomicReference)private boolean CASUpdateTail(Node expect, Node update) {if (tail == expect) {tail = update;return true;}return false;}
解锁的核心是“找到下一个等待线程,通知它可以获取锁”,避免链表中后续线程一直自旋,具体分 3 步(面试时建议结合步骤 + 代码 + 流程图讲解):

同样通过ThreadLocal获取,当前节点持有锁,解锁时需操作它。
•如果next == null:说明当前线程是链表最后一个节点,需要尝试将tail设为null(避免后续线程入队时找不到 prev);
•如果next != null:说明有线程在排队,需要将next.isLocked设为false(通知下一个线程可以获取锁)。
避免 ThreadLocal 内存泄漏,可在解锁后移除当前节点。
解锁的 Java 实现代码如下:
public void unlock() {// 步骤1:获取当前线程的Node节点Node currNode = currentNode.get();// 步骤2:检查是否有下一个等待线程if (currNode.next == null) {// 情况1:没有下一个线程,尝试将tail设为nullif (CASUpdateTail(currNode, null)) {// CAS成功:直接返回(链表已空)return;}// CAS失败:说明有新线程正在入队,需要等待它设置nextwhile (currNode.next == null) {Thread.yield();}}// 情况2:有下一个线程,通知它可以获取锁(设置next.isLocked为false)currNode.next.isLocked = false;// 步骤3:清理当前节点(避免ThreadLocal内存泄漏)currNode.next = null;currentNode.remove();}
这里有个面试易错点:为什么 CAS 更新 tail 失败后要循环等待 next?
答:因为当当前线程(最后一个节点)解锁时,可能有新线程正在执行lock()的步骤 3(设置prev.next = currNode),此时currNode.next还未被赋值,直接解锁会导致新线程永远自旋。因此需要循环等待,直到新线程的next设置完成,再通知它获取锁。
掌握了实现逻辑后,还需要能回答“MCS 锁的核心优势”“与其他锁的区别” 等问题,这些是面试中的加分项。
•无缓存风暴:每个线程只自旋等待“前一个节点的 isLocked”,而不是同一个共享变量,减少 CPU 缓存同步开销;
•公平性:线程按入队顺序获取锁,避免“饥饿”(传统自旋锁可能导致线程一直抢不到锁);
•低无效自旋:只有前一个线程释放锁时,下一个线程才会停止自旋,减少无效 CPU 占用。
CLH 锁(另一种排队自旋锁)与 MCS 锁原理类似,但有一个关键差异:
•自旋对象不同:MCS 锁自旋 “当前节点的 isLocked”,CLH 锁自旋 “前一个节点的 isLocked”;
•适用场景不同:MCS 锁更适合 “非缓存友好” 的环境(如分布式锁),CLH 锁在共享内存环境(如单机器多线程)中缓存效率更高,但 MCS 锁的实现更直观,面试中更常考。
答:JDK 1.6 之后,ReentrantLock的非公平锁实现中,底层的 AQS(AbstractQueuedSynchronizer)队列其实借鉴了 MCS 锁的 “排队思想”——AQS 的Node节点、tail指针、CAS 入队等逻辑,本质上是 MCS 锁的变种(但 AQS 是阻塞锁,不是自旋锁)。面试时提到这一点,能体现你对 JDK 源码的理解。
最后,给大家整理一个“MCS 锁面试答题框架”,按这个逻辑说,既清晰又全面:
1.定义:MCS 锁是排队自旋锁的实现,通过链表记录等待线程,每个线程只自旋前一个线程的释放信号;
2.设计初衷:解决传统自旋锁的“盲等” 和 “缓存风暴” 问题;
3.核心结构:Node节点(isLocked、next)+ 共享 tail 指针 + ThreadLocal 存储当前节点;
4.流程:
•加锁:获取节点→CAS 入队→接入链表→自旋等待前节点;
•解锁:获取节点→检查 next→通知 next 解锁→清理节点;
1.优势:无缓存风暴、公平、低无效自旋;
2.延伸:与 CLH 锁的区别、JDK 中 AQS 的借鉴。
掌握这个框架,再结合代码示例和流程图,MCS 锁相关的面试题就能轻松应对了。
全部0条评论
快来发表一下你的评论吧 !