
你可能见过这样的场景一群人冲进教室座位只有几个谁抢到谁坐抢不到的只能排队等着。Java并发里的ReentrantLock本质上就是在干这件事。不过它的“排队”不是简单的先来后到而是一套基于AQSAbstractQueuedSynchronizer的双向队列机制。这篇文章不打算泛泛而谈直接扒开ReentrantLock源码从“抢座位”这个日常比喻切入把AQS队列从入队到唤醒的每一步拆给你看。适合正在啃并发编程的人、准备Java面试的老哥以及那些想知道锁底层到底怎么转的硬核玩家。1. 从“抢座位”说起ReentrantLock的核心思想1.1 为什么需要一把更“灵活”的锁先问个问题synchronized已经能用为什么还要有ReentrantLock因为synchronized是JVM内置的使用简单但不够灵活。比如你想在抢锁时设置超时时间如果抢不到就去做别的事想响应中断想多个线程交替执行想判断当前是否有线程排队。这些需求synchronized要么不支持要么写起来很别扭。ReentrantLock作为JDK提供的锁本质上就是弥补这些缺口。它叫“Reentrant”是因为同一个线程可以重复获取同一把锁。比如线程拿到锁后再调用一个需要同一把锁的方法不用重新去“抢”只需要给状态值加一。这种设计避免了死锁也让代码更自然。而这一切的核心就是一个叫AbstractQueuedSynchronizer的类简称AQS。ReentrantLock只是AQS的一个应用场景CountDownLatch、Semaphore、ThreadPoolExecutor里的Worker底层都是在用它做同步。1.2 “抢座位”模型状态、Owner与等待队列现在脑子里建立一个模型锁就是一个座位同时只能坐一个人。线程来了先看座位是否空着空着就坐下并把“座位主人”设为自己。这就是state和exclusiveOwnerThread做的事。state锁的状态。0表示没人坐0表示被占用。因为可重入每重入一次就加1。exclusiveOwnerThread记录当前占用锁的线程也就是“座位主人”。如果座位已经有人了后来的线程怎么办两个选择要么直接插队试一下运气要么去队伍末尾排队。ReentrantLock提供了两种模式默认是非公平锁对应“插队模式”也可以通过构造参数true选择公平锁对应“排队模式”。在AQS里排队的线程会进入一个双向队列这个队列的节点是Node每个节点保存了线程引用和等待状态。这就是标题里说的AQS队列。这个队列还有个名字叫CLH锁队列的变体。它并不是操作系统里的那种消息队列也不是BlockingQueue而是一个纯粹的同步控制队列。理解这一点很关键AQS的队列不存业务数据只存“正在等待锁的线程”。2. AQS到底是什么同步器的骨架2.1 核心字段state、head、tail打开AQS源码最先看到的就是几个关键字段。摸清它们你就掌握了80%的脉络。// 锁状态volatile保证可见性 private volatile int state; // 等待队列的头节点 private transient volatile Node head; // 等待队列的尾节点 private transient volatile Node tail;state的作用在上面已经说了它是判断能否获取锁的唯一依据。head和tail则是双向队列的两个端点。队列里每个节点是内部类Node的实例关键字段有volatile int waitStatus; volatile Node prev; volatile Node next; volatile Thread thread;waitStatus是一个状态标记几个关键取值CANCELLED值为1表示该节点被取消线程不再等待锁。SIGNAL值为-1表示后继节点需要被唤醒。也就是说当前节点释放锁或取消时要通知下一个节点。CONDITION值为-2表示节点在条件队列中等待某个条件满足。PROPAGATE值为-3用于共享模式下表示状态需要向后传播。head和tail都有一个特点懒初始化。第一次加锁时如果队列还没建立不会立刻创建而是等到第一个没抢到锁的线程出现时才会通过CAS初始化一个空的头节点再把新线程节点挂到后面。这样做是减少不必要的对象创建。2.2 模板方法模式tryAcquire与acquire的协作AQS最精彩的设计是模板方法模式。它把获取锁和释放锁的整体流程固定好把具体怎么判断“能不能获取”留给子类实现。这样一套模板就能支持各种同步工具。以独占锁为例核心流程是acquire(int arg)public final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }tryAcquire是留给子类实现的钩子方法。ReentrantLock的两个内部类FairSync和NonfairSync分别实现了它从而决定是公平还是非公平。tryAcquire返回true说明锁到手了流程结束返回false则进入下一步创建节点、加入队列、排队等待。这种设计的巧劲在于AQS本身不关心你是锁、信号量还是倒计时器它只提供“排队挂起、唤醒继续”的骨架。子类只需要说清楚“什么条件下算获取成功”其余的全交给AQS。释放锁也有对应的模板release(int arg)public final boolean release(int arg) { if (tryRelease(arg)) { Node h head; if (h ! null h.waitStatus ! 0) unparkSuccessor(h); return true; } return false; }tryRelease也是子类实现负责修改state并清空“Owner线程”。如果释放成功就唤醒等待队列里的下一个线程。3. 源码实战ReentrantLock的加锁与解锁全流程3.1 加锁从小动作到排队的完整路径先看非公平锁的lock()它有个“小动作”final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }刚进入lock()它会先尝试CAS把state从0改成1。如果成功说明座位空着直接抢到手。这就是非公平锁“插队”的第一层体现。如果失败说明锁被占着于是进入acquire(1)。公平锁呢直接调用acquire(1)连“插队”这一步都省了。它得先看队列里有没有人排在自己前面有就老老实实去队尾。acquire里先调用tryAcquire。非公平锁的tryAcquire是这样的final boolean nonfairTryAcquire(int acquires) { final Thread current Thread.currentThread(); int c getState(); if (c 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current getExclusiveOwnerThread()) { int nextc c acquires; if (nextc 0) throw new Error(Maximum lock count exceeded); setState(nextc); return true; } return false; }这里有两个关键分支c 0表示锁空闲再试一次CAS如果锁已经被当前线程持有走重入分支state加一。所以state记录的就是重入次数。如果代码里1个方法里连续锁了3次那state就是3要释放3次才真正释放。tryAcquire失败后进入addWaiter(Node.EXCLUSIVE)private Node addWaiter(Node mode) { Node node new Node(Thread.currentThread(), mode); Node pred tail; if (pred ! null) { node.prev pred; if (compareAndSetTail(pred, node)) { pred.next node; return node; } } enq(node); return node; }这段代码把当前线程包装成Node通过CAS插入到队列尾部。如果tail为空说明队列还没建立走enq方法初始化头节点再自旋插入。注意这里的CAS很重要因为可能有多个线程同时入队必须保证只有一个线程能把节点接到队尾。入队之后线程还没完还要继续尝试获取锁。这就是acquireQueued的职责final boolean acquireQueued(final Node node, int arg) { boolean failed true; try { boolean interrupted false; for (;;) { final Node p node.predecessor(); if (p head tryAcquire(arg)) { setHead(node); p.next null; failed false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) parkAndCheckInterrupt()) interrupted true; } } finally { if (failed) cancelAcquire(node); } }循环里先判断前驱节点是不是head。只有紧挨着头节点的“队首”才有资格再次尝试获取锁。如果队列前面还有人就老老实实休息。这里有个细节setHead(node)会把当前节点设为头节点并清空其thread引用这样原头节点就可以被GC了。如果前驱不是头节点或者尝试获取又失败了就调用shouldParkAfterFailedAcquire检查是否应该挂起线程。它会把前驱节点的waitStatus设成SIGNAL表示“前驱释放锁时记得通知我”。然后通过LockSupport.park挂起线程等待被唤醒。这就是线程从“抢座位”变成“排队睡觉”的过程。3.2 解锁从state归零到唤醒队头有上锁就有解锁。ReentrantLock的unlock()最终调用AQS.release(1)。release先调tryRelease也就是在Sync里实现的protected final 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; }注意两个细节第一如果当前线程不是锁的持有者直接抛出IllegalMonitorStateException绝不让你乱解锁。第二state减去releases后如果变成0才真正释放锁。这也解释了为什么lock()了几次就必须unlock()几次否则锁永远不释放。释放成功后进入unparkSuccessor准备唤醒下一个等待线程private void unparkSuccessor(Node node) { int ws node.waitStatus; if (ws 0) compareAndSetWaitStatus(node, ws, 0); Node s node.next; if (s null || s.waitStatus 0) { s null; for (Node t tail; t ! null t ! node; t t.prev) if (t.waitStatus 0) s t; } if (s ! null) LockSupport.unpark(s.thread); }这里有个容易忽视的坑如果当前节点的后继节点是null或者后继节点被取消了waitStatus 0就必须从队尾往前找找到离头最近的有效节点。为什么要从尾往前因为在并发场景下node.prev的赋值早于node.next的链接往前遍历能避免漏掉节点。这个细节synchronized的维护者们在AQS里处理得相当细腻。3.3 可重入与公平锁的实现差异我们已经看了非公平锁的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; } } else if (current getExclusiveOwnerThread()) { int nextc c acquires; if (nextc 0) throw new Error(Maximum lock count exceeded); setState(nextc); return true; } return false; }区别只在一行!hasQueuedPredecessors()。这个方法会检查队列里是否有比当前线程更早到达的等待者public final boolean hasQueuedPredecessors() { Node t tail; Node h head; Node s; return h ! t ((s h.next) null || s.thread ! Thread.currentThread()); }如果队里已经有人在等待公平锁就直接放弃争夺去队尾排队。非公平锁则不管这些逮着机会就CAS。所以公平锁的吞吐量通常比非公平锁低但线程排队更有序不会出现“刚来的线程把老线程饿死”的情况。可重入是两者共有的能力。所谓重入就是同一个线程再次执行lock()时state会递增而不需要真正去“抢座”。这依赖exclusiveOwnerThread记录当前持有线程。重入次数用state保存释放时逐层递减。4. 从AQS到并发工具箱阻塞队列与线程池的关联4.1 AQS队列 vs 阻塞队列不同的“排队”机制很多人刚接触AQS时会把它跟BlockingQueue搞混。实际上两者完全是两回事。AQS队列是同步队列存放的是等待获取锁的线程节点它不承载业务数据。它的操作是CAS入队、LockSupport.park挂起、unpark唤醒。而ArrayBlockingQueue、LinkedBlockingQueue这类阻塞队列是用来存放业务数据的生产者和消费者通过它传递信息。阻塞队列内部同样会用ReentrantLock和Condition来控制并发比如ArrayBlockingQueue源码里就有private final ReentrantLock lock; private final Condition notEmpty; private final Condition notFull;它用同一个锁的两把“条件钥匙”分别管理“队空”和“队满”。当队列为空时消费者线程在notEmpty.await()上挂起等生产者放入数据后signal()唤醒。这套机制本质上就是AQS的ConditionObject实现的。所以可以这样理解AQS是地基BlockingQueue是盖在上面的房子。你在用线程池时选哪种阻塞队列实际是在选房子的“缓冲策略”——有界、无界、还是直接丢弃。4.2 线程池里的AQS应用线程池ThreadPoolExecutor里也藏着AQS。它的内部类Worker继承了AbstractQueuedSynchronizer用来表示一个工作线程是否空闲。private final class Worker extends AbstractQueuedSynchronizer implements Runnable { // 仅实现 tryAcquire / tryRelease用 state 判断是否空闲 }Worker通过tryAcquire把state从0改1来标记自己正在执行任务执行完后再tryRelease释放。这样做是为了实现shutdown()时能中断空闲线程而不误伤正在跑任务的线程。这里没有复杂的排队只是借用AQS的独占语义做状态控制。另外线程池的execute命令提交任务时如果核心线程满了任务会进入workQueue。这个workQueue就是阻塞队列。选择不同的阻塞队列会对线程池行为产生很大影响。比如LinkedBlockingQueue默认无界可能导致任务堆积过多ArrayBlockingQueue有界配合CallerRunsPolicy能缓解堆积压力SynchronousQueue不存任务线程不够就拒绝。顺带一提关于消息队列重复消费问题那已经是中间件层面的东西了和AQS关系不大但如果你理解了AQS里的CLH队列是怎么通过CAS保证线程安全的再去看RocketMQ、Kafka的消费位点提交会更容易理解“幂等”在分布式场景下的意义。5. 常见问题与排查实录5.1 公平锁和非公平锁该怎么选这几乎是面试必问的问题。非公平锁性能更好因为线程从park状态被唤醒后还要重新参与CAS竞争而新来的线程直接在用户态就尝试了一次省去了线程上下文切换的延迟。但非公平锁也带来了“饥饿”风险——老线程可能一直等不到锁。如果业务对响应时间有严格要求或者希望每个线程的机会尽量均匀就选公平锁。但如果追求吞吐量、竞争激烈程度一般非公平锁的默认选择是合理的。实测下来大多数互联网后端场景用非公平锁就够了因为锁持有时间通常很短。5.2 锁中断与超时AQS的另一面ReentrantLock还提供了lockInterruptibly()和tryLock(timeout, unit)。前者能让等待锁的线程响应中断后者能在等待超时后放弃。它们的实现都在AQS的doAcquireInterruptibly和tryAcquireNanos里。比如tryLock带超时最终会调用private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { // 每次循环检查剩余时间超时则返回 false if (nanosTimeout 0L) return false; // 如果等待超时走 cancelAcquire 取消排队 }它和acquireQueued最大的区别在于循环中会计算剩余时间并且支持响应中断。如果你在代码里用lockInterruptibly()线程被中断时会抛出InterruptedException然后做节点取消清理。一个常见踩坑场景是线程在parkAndCheckInterrupt()中被挂起调用Thread.interrupt()会唤醒它但park不会抛出异常。AQS会在循环里检测到中断标记返回true最终由selfInterrupt()重新设置中断标志。所以你一定要在业务代码中及时处理中断状态否则标志会一直残留。5.3 源码阅读心得与避坑指南读AQS源码我最大的体会是不要试图一次看懂所有分支先把“队列头尾、节点状态、CAS、park/unpark”这四个概念贯穿起来。可以用一张草图画出来线程A在座位上线程B排队线程C入队。然后跟着acquireQueued跑一遍循环你会发现所有分支都是在处理“队列空不空、前驱状态是不是SIGNAL、要不要取消”这几种情况。另一个容易忽略的坑Node从队尾往前遍历时prev指针是可靠的但next指针可能因为并发入队而暂时为null。所以在unparkSuccessor里才需要特殊处理。你要是写自定义同步器千万别一味相信next。最后建议你打开源码自己跑一遍把state的变化、节点的入队出队过程用日志打出来。纸上得来终觉浅源码这东西亲手扒过一遍和只看别人文章体感完全两样。我在实际阅读代码时还会顺手在addWaiter和acquireQueued加几个断点用两个线程同时竞争锁观察它们如何进入队列、如何被唤醒。这个动手过程比任何解说都管用。你现在可以打开ReentrantLock源码从lock()一路追到parkAndCheckInterrupt()相信看完以后你再看到“锁”、“队列”、“阻塞”这些词会多一层底层画面的直觉。