☰
AQS-ReentrantLock
2026/10/22 2:28:52 网站建设 项目流程

经典 CLH 队列结构

CLH 锁是一个隐式的 FIFO 队列。其结构如下:

  • 节点(QNode):每个线程在获取锁时,会将其关联到一个节点上。节点结构很简单,通常包含两个关键属性:
    locked:一个 volatile boolean 变量,表示该节点的锁持有状态。
    prev:一个指向前驱节点的引用,这是构成队列的基础。
  • 队列尾指针:维护一个全局的尾指针 tail,它始终指向最后一个入队的节点。

锁获取与释放过程

入队(加锁):

线程创建一个新的节点 myNode,将其 locked 初始化为 true。
通过原子操作 getAndSet(tail, myNode),将 myNode 设为新的 tail,并获取之前的前驱节点 pred。
如果 pred 不为空(即队列中已有等待者),当前线程就在前驱节点的 locked 字段上自旋等待:while (pred.locked) {}。
如果 pred 为空(即队列为空),则直接获取锁。

出队(解锁):

持有锁的线程将自己的节点 myNode 的 locked 设置为 false。
此时,自旋在它上面的后继线程会发现 pred.locked 变为 false,停止自旋并成功获取锁。锁的传递是隐式的。

作者:敖正炀
链接:https://juejin.cn/post/7652180405776842762
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

AQS 的三大改造:自旋改阻塞、单向改双向、局部改全局

改造一:自旋改阻塞(park/unpark)

这是最核心的改造。将 CLH 原型的忙等自旋 while(pred.locked){} 替换为 LockSupport.park(this),使线程进入 WAITING 状态并让出 CPU。
当前节点在释放锁时必须去唤醒后继节点。

改造二:单向链表改双向链表

为了支持显式的唤醒和节点取消,AQS 的 CLH 变体必须是一个双向链表。
(1)增加 next 指针:

  • 用于唤醒:在 unparkSuccessor 方法中,当头节点(或任何释放锁的节点)需要唤醒其后继时,它可以直接通过 head.next 找到下一个应该被唤醒的节点。
  • 用于取消重连:在 cancelAcquire 方法中,当一个节点被取消时,它需要通过 prev 找到它的有效前驱 pred,然后将 pred.next 指向它的后继节点 next,从而将自己从队列中移除。这是“自愈”能力的基础。

改造三:从局部队列到全局 FIFO 队列

CLH 理论原型的队列是隐式的,只通过每个线程私有的节点和它们之间的 prev 引用构成,没有集中的全局队列视图。AQS 将其改造为具有明确的 head 和 tail 全局指针的显式队列。
(1)head 哨兵节点:head 指向的节点并不代表一个等待者,而是代表当前已持有锁的线程(或其占位符)。这个节点中的 thread 为 null。这种设计有多个好处:

  • 简化边界条件:队列永远不会为空(head 总是指向一个哨兵),避免了大量对 head == null 的检查。
  • GC 友好:在 setHead 方法中,旧的头节点会被清空 thread 和 prev 引用,使其成为不持有任何线程引用的孤立节点,便于垃圾回收。

(2)tail 指针与 CAS 入队:新节点通过 CAS 操作原子性地更新全局的 tail 指针来加入队列。addWaiter 和 enq 方法负责在高并发下安全地完成节点插入,形成了清晰、可管理的 FIFO 队列视图。

作者:敖正炀
链接:https://juejin.cn/post/7652180405776842762
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

以下基于jdk17,与jdk1.8版本的有区别

ReentrantLock

ReentrantLock默认创建的是非公平锁。

加锁-初步尝试加锁


initialTryLock()中:公平锁如果等待队列中有正在等的元素就不抢了;非公平锁上来就抢。

tryAcquire()再尝试获取一次,尝试不成功再往下执行acquire()方法。

公平锁


公平锁发现state=0的时候:
如果队列里没有其他正在等待获取锁的Node,通过cas抢占锁,抢占成功把排它锁的拥有者改成自己。
或者排它锁的拥有者已经是自己了,那就给state+1。
否则抢占失败。

非公平锁


上来就抢。

加锁-不停地循环尝试加锁

// AbstractQueuedSynchronizer.javafinalintacquire(Nodenode,intarg,booleanshared,booleaninterruptible,booleantimed,longtime){Threadcurrent=Thread.currentThread();bytespins=0,postSpins=0;// retries upon unpark of first threadbooleaninterrupted=false,first=false;Nodepred=null;// predecessor of node when enqueued/* * Repeatedly: * Check if node now first * if so, ensure head stable, else ensure valid predecessor * if node is first or not yet enqueued, try acquiring * else if node not yet created, create it * else if not yet enqueued, try once to enqueue * else if woken from park, retry (up to postSpins times) * else if WAITING status not set, set and retry * else park and clear WAITING status, and check cancellation */for(;;){if(!first&&(pred=(node==null)?null:node.prev)!=null&&!(first=(head==pred))){if(pred.status<0){cleanQueue();// predecessor cancelledcontinue;}elseif(pred.prev==null){Thread.onSpinWait();// ensure serializationcontinue;}}if(first||pred==null){booleanacquired;try{if(shared)acquired=(tryAcquireShared(arg)>=0);elseacquired=tryAcquire(arg);}catch(Throwableex){cancelAcquire(node,interrupted,false);throwex;}if(acquired){if(first){node.prev=null;head=node;pred.next=null;node.waiter=null;if(shared)signalNextIfShared(node);if(interrupted)current.interrupt();}return1;}}if(node==null){// allocate; retry before enqueueif(shared)node=newSharedNode();elsenode=newExclusiveNode();}elseif(pred==null){// try to enqueuenode.waiter=current;Nodet=tail;node.setPrevRelaxed(t);// avoid unnecessary fenceif(t==null)tryInitializeHead();elseif(!casTail(t,node))node.setPrevRelaxed(null);// back outelset.next=node;}elseif(first&&spins!=0){--spins;// reduce unfairness on rewaitsThread.onSpinWait();}elseif(node.status==0){node.status=WAITING;// enable signal and recheck}else{longnanos;spins=postSpins=(byte)((postSpins<<1)|1);if(!timed)LockSupport.park(this);elseif((nanos=time-System.nanoTime())>0L)LockSupport.parkNanos(this,nanos);elsebreak;node.clearStatus();if((interrupted|=Thread.interrupted())&&interruptible)break;}}returncancelAcquire(node,interrupted,interruptible);}

在这个循环中,不停地完善此线程对应的node的属性,以及在抢锁的列表中的位置:

  • 如果当前node现在是第一个元素了,就开始抢锁。抢锁成功后,当前node成为头结点,如果是共享锁就唤醒它的下一个节点。
  • 创建Node;
  • 从尾部加入抢锁列表;
  • 当前Node是第一个元素,且spins != 0,让线程先:Thread.onSpinWait();
    底层(x86 平台)
    编译后变成PAUSE指令:
    (1)告诉 CPU:我在自旋循环,短等,不要疯狂乱猜内存顺序、不要持续抢占流水线
    (2)短暂停顿几十个 CPU 周期,降低功耗、减少超线程(HT)下对另一个逻辑核的争抢
    (3)避免 CPU 的内存顺序推测惩罚(memory-order misprediction)
    ARM/AArch64 一般是 YIELD 指令,部分平台直接 no-op,啥也不做cr.openjdk…
  • 让当前node状态变成wating,也就是1;
  • 以上条件都不符合,自旋时间*2倍。且阻塞当前线程,如果线程被中断则退出循环抢锁。
    LockSupport.park() / wait() / sleep()
    (1)线程进入阻塞(WAITING),内核态
    (2)上下文切换开销很大,适合等待时间不确定、比较久的场景

释放锁




state-1,state=0时,把排它锁的拥有者置为null。

s.getAndUnsetStatus(WAITING);的作用是把status的状态恢复成0。
唤醒下一个处于WATING状态的节点。

Thread.onSpinWait()

先分清楚两个概念:物理核、逻辑核(超线程 HT)
现代 CPU 超线程:一个物理核,包含两套寄存器,分成 2 个逻辑核,共享同一个流水线、执行单元

1. 单独看【这个执行 PAUSE 的逻辑核】

当 CPU 跑PAUSE:
这个逻辑核,不会继续往流水线预取、乱序猜测后面的循环指令,它会等待一小段周期(几十 cycle),再重新取指令、判断 flag。在这短暂的几十 cycle 里:这个逻辑核不再推进自己的代码
但是!如果这个物理核还有另外一个空闲的逻辑核(超线程):物理核的执行单元,可以调度给另一个逻辑线程去跑指令!
这正是 PAUSE 设计的核心目的:减少自旋对同物理核另一个逻辑线程的干扰。

2. 两种场景拆开讲

场景 A:物理核只有这一个逻辑核(关闭超线程)

执行 PAUSE 的这段周期:
这个核的流水线,不再取这个自旋循环后面的指令。没有别的逻辑线程,硬件单元就空等一会儿。
这个核在这段时间几乎不干活。

场景 B:开启超线程(最常见),1 物理核 = 2 逻辑核

逻辑核 A:自旋、执行 PAUSE
逻辑核 B:跑别的业务线程
PAUSE 生效时:
逻辑核 A 主动抑制激进预取和乱序,让出共享的流水线资源给逻辑核 B去执行指令。
物理核并没有闲着,另一个逻辑线程可以继续跑!

3. 最容易踩坑的误区(面试必区分)

❌ 错误理解:PAUSE → 整个 CPU 内核暂停,啥指令都不跑
✅ 正确理解:
PAUSE 是给硬件的提示:我这个逻辑线程正在自旋等待,不要拼命预取、推测执行我的后续指令;短暂 hold 住本逻辑线程的流水线,释放共享资源给同核上另一个逻辑线程。

它不是操作系统层面的线程切换,只是 CPU 硬件流水线层面的让步,没有上下文切换,开销极低。

4. 和 park 做一个对比帮你巩固

  • PAUSE(onSpinWait):线程仍然是 RUNNABLE,逻辑核归属没变,只是硬件暂缓自己的流水线,同物理核其他逻辑线程可以蹭资源
  • park:线程进入 WAITING,操作系统把这个线程从 CPU 上踢下去,这个逻辑核可以被任意其他线程抢占(真正的调度切换,开销大)

一句话总结背诵

PAUSE 不会让整个 CPU 物理核停止工作;只是当前这个逻辑线程停止继续预取、推测执行自己的指令,短暂暂缓本逻辑线程流水线;如果开启超线程,同一个物理核上另一个逻辑线程可以利用这段时间执行任务。

区分

  1. Thread.onSpinWait()
    • 线程仍然 RUNNING,不放弃 CPU 时间片
    • 仅给 CPU 提示,短暂停顿几个周期
    • 适合:极短等待(几 ns~ 几百 ns),自旋锁、AQS 抢锁快路径
    • 不会让出 CPU,不会进入内核态
  2. Thread.yield()
    • 告诉调度器:我愿意让出时间片,回到就绪队列
    • 可能被立刻再次调度回来;会触发调度器
    • 代价比 onSpinWait 高很多
  3. LockSupport.park() / wait() / sleep()
    • 线程进入阻塞(WAITING),内核态
    • 上下文切换开销很大,适合等待时间不确定、比较久的场景

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询