社招简历别瞎写,搞定这3个高频面试题,原理不再一问三不知
面试被问原理答不上来,是不是让你瞬间大脑空白?社招简历里堆砌的项目经验,在面试官深挖底层逻辑时,往往显得苍白无力。
很多开发者以为,只要把“精通Java”、“熟悉Redis”写进社招简历,就能拿到高薪Offer。大错特错。面试官要看的不是你会多少框架,而是你懂不懂背后的高频面试题背后的设计思想。
今天不聊虚的,直接拆解一个在社招中极高频出现的场景:高并发下的数据一致性。我们将以Java并发编程中的核心组件 ReentrantLock 为例,剖析其源码实现,看看那些让你答不上来的原理,到底藏在哪里。
1. 入口定位:从AQS到状态锁
为什么社招简历里写“熟悉并发”,面试官就要问锁?因为锁是并发编程的基石。
如果你只停留在 synchronized 关键字的使用上,那只能算入门。在JDK 1.5之后,java.util.concurrent 包提供了更强大的工具,其中 ReentrantLock(可重入锁)是最核心的类之一。
要理解它,必须先了解它的爹——AQS (AbstractQueuedSynchronizer)。AQS是JUC包中所有同步器实现的基类,它维护了一个 volatile int state 变量和一个CLH双向队列。
在 ReentrantLock 中,state 代表锁的重入次数:state == 0:锁未被占用。
state 0:锁被占用,值为重入次数。当线程尝试获取锁时,实际上是在修改这个 state 变量。如果 state 为0,通过CAS原子操作将其置为1,则获取成功;否则,进入阻塞队列等待。
2. 核心片段:尝试获取锁的真相
很多同学在面试中说:“我知道是CAS原子操作。”但CAS失败后怎么办?线程怎么排队?怎么唤醒?这些细节才是区分初级和高级的分水岭。
让我们打开 ReentrantLock 的源码(建议对照官方源码仓库 JDK 17 版本查看)。
核心逻辑在 NonfairSync(非公平锁,默认)的 lock() 方法中:
// 语言: Java
// 来源: java.util.concurrent.locks.ReentrantLock$NonfairSync
public void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elsedoAcquireInterruptibly(acquire(1));
}逐行拆解:compareAndSetState(0, 1):这是关键。它尝试将AQS的 state 从0改为1。这是一个原子操作,没有加锁。如果成功,说明当前没有线程持有锁,当前线程直接获得锁。
setExclusiveOwnerThread(Thread.currentThread()):记录当前持有锁的线程。这是为了支持可重入性。如果同一个线程再次请求锁,AQS会检查 state 是否大于0,以及持有者是否是当前线程,如果是,则直接 state++,实现重入。
else doAcquireInterruptibly(acquire(1)):如果CAS失败,说明锁已被其他线程占用。此时线程不会自旋等待,而是被包装成一个节点,加入到AQS的等待队列中,然后进入 park 状态,释放CPU资源。这里有一个高频面试题陷阱:为什么默认是非公平锁?
答:非公平锁在CAS失败时,允许新线程直接尝试获取锁(虽然代码里这里直接跳过了,但在 fairLock 中会有不同处理,且非公平锁减少了线程上下文切换开销,吞吐量更高)。
3. 设计思想:CAS + 状态机 + 队列
ReentrantLock 的设计思想可以概括为三点:CAS原子操作、状态机模式、CLH队列同步。
1. CAS原子操作
state 的修改必须原子化,否则在多线程下会出现竞态条件。Unsafe 类的 compareAndSwapInt 方法保证了这一点。
2. 状态机模式
AQS的 state 就像一个状态机。空闲状态:state == 0
占用状态:state 0
中断状态:通过 Thread.interrupt() 标志位处理,虽然 state 不变,但线程会在唤醒时检查中断标志。这种设计将“锁的占用情况”与“线程的排队情况”解耦。锁的状态由 state 维护,线程的排队由队列维护。
3. CLH队列同步
当线程获取锁失败时,它被封装成一个 Node 对象,通过 enqueue 方法加入队列尾部。这个队列是双向链表,头节点是 head(虚拟节点,或持有锁的节点),尾节点是 tail。
源码片段2:入队逻辑
// 语言: Java
// 来源: java.util.concurrent.locks.AbstractQueuedSynchronizer
private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);// Fast path for enq, split out to improve performanceNode pred = tail;if (pred == null) {enq(node); // 如果队列为空,初始化队列} else {node.prev = pred;if (!compareAndSetTail(pred, node)) {enq(node); // CAS失败,自旋重试}}return node;
}逐行拆解:new Node(Thread.currentThread(), mode):创建节点,记录当前线程和模式(SHARED或EXCLUSIVE)。
Node pred = tail:获取当前尾节点。
if (pred == null):如果是第一个节点,调用 enq 方法。enq 内部会使用自旋CAS来设置 head。
node.prev = pred:建立前向链接。
if (!compareAndSetTail(pred, node)):尝试将 tail 指向新节点。如果失败,说明有其他线程并发修改了 tail,进入 enq 方法自旋重试,直到成功。这个自旋过程看似简单,实则保证了在高并发下,队列结构的完整性。
4. 手写简化版:理解核心逻辑
为了加深理解,我们可以手写一个极简版的 ReentrantLock,忽略中断处理、公平锁选择等复杂逻辑,只保留核心骨架。
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;// 简化版可重入锁
public class SimpleReentrantLock implements Lock {private static class SimpleSync extends AbstractQueuedSynchronizer {// 获取锁@Overrideprotected boolean tryAcquire(int arg) {if (arg != 1) throw new IllegalArgumentException();Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, 1)) {setExclusiveOwnerThread(current);return true;}} else if (current == getExclusiveOwnerThread()) {int nextc = c + 1;if (nextc 0) throw new Error(Maximum lock count exceeded);setState(nextc);return true;}return false;}// 释放锁@Overrideprotected boolean tryRelease(int arg) {if (arg != 1) throw new IllegalArgumentException();if (!isHeldExclusively()) throw new IllegalMonitorStateException();int nextc = getState() - 1;setState(nextc);if (nextc == 0) setExclusiveOwnerThread(null);return nextc == 0;}// 创建条件对象Condition newCondition() {return new ConditionObject();}}private final SimpleSync sync = new SimpleSync();@Overridepublic void lock() {sync.acquireSharedInterruptibly(1); // 注意:这里简化处理,实际非公平锁用 acquire}@Overridepublic void unlock() {sync.releaseShared(1);}@Overridepublic void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);}@Overridepublic boolean tryLock() {return sync.tryAcquire(1);}@Overridepublic boolean tryLock(long timeout, java.util.concurrent.TimeUnit unit) throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(timeout));}@Overridepublic Condition newCondition() {return sync.newCondition();}
}代码解析:tryAcquire:核心逻辑。如果 state==0,CAS置1并记录线程;如果是同一线程,state++。
tryRelease:核心逻辑。检查是否是持有者,state--,如果为0,清空线程引用。
lock/unlock:委托给内部的 SimpleSync 对象处理。通过这个简化版,你可以清晰地看到:锁的本质是对共享资源 state 的原子修改。
5. 应用场景:社招简历如何体现深度
在社招简历中,不要只写“使用Redis分布式锁”。你应该写:“基于 Redisson 实现分布式锁,深入理解其底层 Redis 命令 SETNX 与 Lua 脚本的结合,解决锁误删问题。”
“在高并发场景下,优化 ReentrantLock 的使用,通过减少锁粒度(从方法级锁改为对象级锁)提升吞吐量,QPS提升30%。”避坑指南:不要滥用锁:能用 ConcurrentHashMap 解决的,不要用 synchronized 包裹整个方法。
注意死锁:多个锁同时获取时,务必保证获取顺序一致。
公平与非公平:默认非公平锁性能更好,但在某些严格公平场景(如队列服务)下,需使用 new ReentrantLock(true)。高频面试题复盘:Q: synchronized 和 ReentrantLock 的区别?
A: synchronized 是JVM层面的锁,自动释放;ReentrantLock 是API层面的锁,需手动 unlock,支持中断、公平锁、多个 Condition。
Q: AQS 的原理?
A: 基于 volatile int state 和 CLH 队列,通过 CAS 操作修改 state,线程排队等待。面试被问原理答不上来,往往是因为只知其然,不知其所以然。源码是最好的老师。
你公司项目里是怎么处理的?欢迎评论。
