别再死磕AQS源码了!用银行排队场景图解ReentrantLock非公平锁的加锁全过程

张开发
2026/4/6 22:29:45 15 分钟阅读

分享文章

别再死磕AQS源码了!用银行排队场景图解ReentrantLock非公平锁的加锁全过程
银行排队模型拆解ReentrantLock用生活场景秒懂非公平锁的加锁逻辑想象一下周五下午的银行网点三个窗口只开放一个等待区坐满了人叫号机不断刷新号码。这正是ReentrantLock非公平锁的完美隐喻——线程争夺锁资源的过程就像顾客争夺银行柜台的办理权。我们将用这个鲜活场景带你透视AQS核心机制无需逐行啃源码就能建立立体认知。1. 银行模型与AQS核心组件映射银行的基础设施与AQS的各个组件存在精准对应关系银行元素AQS组件技术含义柜台服务窗口state状态变量用volatile int记录锁占用状态0空闲1占用1重入叫号机CLH队列虚拟双向链表维护等待线程采用FIFO原则顾客填写的申请表Node节点封装线程信息Thread引用、等待状态waitStatus大堂经理CAS操作原子性协调state修改和队列更新等候区广播系统LockSupport.park/unpark阻塞/唤醒线程的底层机制当线程A调用lock()时相当于顾客直接走向空闲窗口// 非公平锁尝试直接抢占 final void lock() { if (compareAndSetState(0, 1)) // 类似查看柜台是否空闲 setExclusiveOwnerThread(Thread.currentThread()); // 占据窗口 else acquire(1); // 进入排队流程 }关键差异点非公平锁允许新来的线程插队尝试获取锁就像VIP客户可以不取号直接询问窗口是否空闲。这种设计虽然可能造成线程饥饿但减少了线程切换开销整体吞吐量更高。2. 非公平锁的完整加锁流程拆解2.1 第一轮争夺尝试直接获取锁当ABC三个线程先后到达银行调用lock()线程A第一个顾客查看state0柜台无人通过CAS将state改为1无需排队直接办理设置exclusiveOwnerThread为自己窗口显示正在服务A线程B到达时CAS修改state失败A正在办理进入acquire(1)流程public final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }2.2 排队机制CLH队列的运作线程B的addWaiter()过程就像取号机生成排队号码创建Node节点申请单填写线程B信息通过CAS将节点追加到队列尾部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; }队列初始化后的结构傀儡节点waitStatus0 ←→ B节点waitStatus0 ↑虚拟头节点 ↑真实第一个等待线程注意CLH队列的第一个节点永远是dummy节点它的存在简化了边界条件处理类似银行叫号系统的初始1号。2.3 排队中的状态管理线程B进入acquireQueued后的关键步骤检查前置节点确认自己是第一个有效等待节点前驱是head再次尝试获取锁非公平锁的二次机会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; // help GC return interrupted; } if (shouldParkAfterFailedAcquire(p, node) parkAndCheckInterrupt()) interrupted true; } } finally { if (failed) cancelAcquire(node); } }挂起线程通过LockSupport.park()进入等待类似顾客在等候区睡觉此时线程C加入队列傀儡节点waitStatus-1 ←→ B节点waitStatus-1 ←→ C节点waitStatus0 ↑SIGNAL状态表示需要唤醒后继 ↑等待被唤醒3. 解锁与唤醒的连锁反应当线程A完成业务调用unlock()释放锁资源public void unlock() { sync.release(1); } public final boolean release(int arg) { if (tryRelease(arg)) { // 将state从1改为0 Node h head; if (h ! null h.waitStatus ! 0) unparkSuccessor(h); // 唤醒后继节点 return true; } return false; }唤醒线程B的具体过程检查傀儡节点的waitStatus-1需要通知后继从尾部向前遍历找到离head最近的有效节点处理取消情况的健壮性设计调用LockSupport.unpark(threadB)线程B上位后的队列变化B节点升级为新的dummy节点thread属性置空原B节点的next指针断开帮助GC回收队列变为新傀儡节点原B节点 ←→ C节点4. 非公平锁的典型问题与实战建议4.1 线程饥饿现象就像频繁到来的VIP客户可能导致普通顾客长时间等待非公平锁在高并发场景下可能造成新线程持续插队成功早先排队的线程长期得不到执行优点减少线程切换开销提高吞吐量解决方案对比策略实现方式适用场景改为公平锁构造ReentrantLock(true)需要严格顺序的执行流设置最大等待时间tryLock(long timeout)允许部分任务降级处理分级锁不同优先级使用不同锁多级别任务并存的系统4.2 调试技巧与工具队列状态监控// 获取等待队列长度 int queueLength lock.getQueueLength(); // 判断是否有线程在等待 boolean hasQueuedThreads lock.hasQueuedThreads();线程堆栈分析# Linux下查看线程状态 jstack pid | grep -A 1 parking to wait for可视化工具推荐JConsole观察线程阻塞情况Arthas的thread -b命令检测死锁YourKit分析锁竞争热点5. 从银行模型看AQS的设计哲学通过这个生活化类比我们可以提炼出AQS的三大精妙设计状态与队列分离state变量实现快速尝试柜台状态即时可见CLH队列处理竞争排队等候区秩序维护模板方法模式// 需要子类实现的核心操作 protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); } protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }性能与公平的权衡非公平锁类似急诊优先机制整体效率优先公平锁类似严格叫号系统顺序性有保障这种设计使得AQS成为Java并发包的基石衍生出CountDownLatch、Semaphore等同步工具。理解了这个模型下次当你看到AbstractQueuedSynchronizer时脑海中会自动浮现银行里人来人往的画面——技术原理原来如此贴近生活。

更多文章