别再死记硬背AQS了!我用银行排队办业务的故事,带你吃透ReentrantLock非公平锁源码
银行排队办业务用生活故事彻底理解ReentrantLock非公平锁走进银行大厅取号机前总会上演着相似的场景——有人径直走向空闲窗口有人默默排队等候。这种日常现象与Java并发编程中的ReentrantLock非公平锁机制惊人地相似。本文将用银行办理业务的完整故事线带你透视AQS框架的核心设计。1. 银行营业厅的并发世界现代银行营业厅就是一个天然的并发系统多个窗口CPU核心并行处理业务线程任务取号机AQS协调顾客线程的排队顺序。当某个窗口空闲时系统需要决定是将机会给新来的顾客还是排队已久的顾客这正是公平与非公平调度的本质区别。关键角色对照表银行场景AQS概念技术实现营业窗口共享资源state变量取号机CLH队列Node链表顾客竞争线程Thread对象叫号系统线程调度LockSupport机制VIP插队非公平锁tryAcquire策略在非公平锁模式下新来的顾客线程不必遵守先来后到的规则这与我们实际银行中遇到的以下场景一致刚进门的顾客发现窗口空闲直接办理无需取号正在排队的顾客必须等待前一位业务办理完成窗口服务结束后会优先检查是否有新顾客到达// 典型非公平锁使用场景 ReentrantLock lock new ReentrantLock(false); // 参数false表示非公平模式 void businessProcess() { lock.lock(); try { // 临界区业务操作 System.out.println(Thread.currentThread().getName() 正在办理业务); TimeUnit.SECONDS.sleep(1); } finally { lock.unlock(); } }2. CLH队列的排队艺术银行的高效运营离不开科学的排队管理AQS的CLH变种队列也是如此。这个虚拟队列通过两个指针head/tail维护具有以下特性哨兵节点设计就像银行排队区第一个位置永远空着CLH队列的首节点不承载实际线程状态位机制每个顾客的号码牌Node.waitStatus记录着等待状态0初始状态-1SIGNAL表示后续节点需要唤醒-2CONDITION处于条件等待1CANCELLED已取消排队非公平锁的排队流程新顾客A进入银行发现窗口空闲state0直接尝试抢占窗口CAS修改state若抢占失败取号加入队列尾部addWaiter进入自旋检查状态acquireQueued前驱节点完成业务后触发唤醒unparkSuccessor重要提示与公平锁不同非公平锁在lock()阶段就会直接尝试CAS抢锁不检查队列状态。这种设计虽然可能造成插队现象但显著减少了线程切换开销。3. 状态管理的精妙设计银行窗口的正在服务指示灯对应着AQS的核心——state变量。这个int值通过volatile保证可见性配合CAS操作实现原子更新// ReentrantLock中Sync类的实现片段 final boolean nonfairTryAcquire(int acquires) { final Thread current Thread.currentThread(); int c getState(); // 读取当前状态 if (c 0) { // 窗口空闲 if (compareAndSetState(0, acquires)) { // CAS抢锁 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; }状态变化的典型场景初始状态state0窗口空闲无顾客线程A获取锁state1窗口占用线程A重入state2同一线程多次lock线程A释放锁state1→最终到0完全释放4. 解锁与唤醒的连锁反应当顾客完成业务离开窗口时银行经理需要协调后续服务流程这与unlock()的唤醒机制高度一致释放资源tryRelease()将state递减完全释放时清零检查等待队列unparkSuccessor()找到最前端的有效节点唤醒后续线程LockSupport.unpark()唤醒对应线程队列维护新获锁的线程成为新的哨兵节点// 解锁流程核心代码示例 public final boolean release(int arg) { if (tryRelease(arg)) { // 尝试释放 Node h head; if (h ! null h.waitStatus ! 0) unparkSuccessor(h); // 唤醒后继 return true; } return false; } 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); // 唤醒 }在实际项目中理解这些机制对排查死锁、线程饥饿问题至关重要。曾经遇到过一个性能问题某高频交易系统使用默认的非公平锁在负载激增时出现了部分请求长时间等待。通过增加队列监控发现某些快速执行的短任务不断插队导致长任务迟迟得不到执行。最终采用混合策略——对关键路径使用公平锁普通操作使用非公平锁取得了较好的平衡。