当前位置: 首页 > news >正文

AQS公平锁与非公平锁之源码解析

AQS加锁逻辑

ReentrantLock.lock

    public void lock() {sync.acquire(1);}

AbstractQueuedSynchronizer#acquire

    public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

addWaiter就是将节点加入队列的尾部,我们先看看非公平锁NonfairSynctryAcquire

	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) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
  • 可以看到是尝试cas获取锁,获取到了将当前线程设置为持有锁的线程
  • 在AQS中,有一个STATE变量,当为1时表示该锁被占用,所以cas的是这个status值
  • 如果cas失败,就会回到acquire方法,继续调用acquireQueued

公平锁fairSynctryAcquire

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;}
  • 和非公平锁的区别:就是在tryAuquire时会先进行hasQueuedPredecessors,即判断当前是否有节点在队列里,有的话不参与cas竞争,实际上后续的解锁和唤醒操作,对于是否公平都是一样,只有这里体现了公平与非公平的区别,对于公平锁,当解锁唤醒队列中的节点时,此时新的获取锁的请求不会与队列中的节点竞争,保证队列中的节点优先唤醒,即保证了FIFO

AbstractQueuedSynchronizer#acquireQueued

![[Pasted image 20250121170956.png]]

  1. 再一次调用tryAcquire这个方法,尝试获取锁,获取成功后将该节点设置为头节点,两个结论:1. cas失败两次会进行阻塞,2. 链表中持有锁的节点就是头节点
  2. 如果失败就会进入shouldParkAfterFailedAcquire
   private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL)return true;if (ws > 0) {do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {pred.compareAndSetWaitStatus(ws, Node.SIGNAL);}return false;}

![[Pasted image 20250121171505.png]]

  • 五个状态:
状态作用
CANCELLED1表示该节点已经取消等待,不再参与锁竞争
SIGNAL-1表示后续节点需要被唤醒
CONDITION-2该节点在等待条件队列中
PROPAGATE-3共享模式下传播信号,让后续线程继续执行
默认值0节点刚入队列时的状态,当节点是队尾,状态就是0
  • 回到shouldParkAfterFailedAcquire方法,当前驱节点状态时SINGAL时,说明前驱节点将锁释放了,将唤醒当前节点(唤醒意味着该节点重新参与竞争锁,因为如果是非公平锁仍需要竞争),当前的线程不应该park,应该回到前面的for循环继续tryacquire
  • 如果状态大于0,说明前驱节点已经cancel,这个节点应该移除链表,可以看到这里的while会将其移除链表
  • 如果状态此时小于0等于了,说明这个前驱节点是正常的,将其设置为SINGAL状态,意为下次会唤醒当前节点
  1. parkAndCheckInterrupt,这里就真正进行阻塞了,所以当前驱节点唤醒当前节点时,回到这个位置,重新开始for循环,acquire尝试获取锁

park与sleep的区别:可以被另一个线程调用LockSupport.unpark()方法唤醒;线程的状态和wait调用一样,都是进入WAITING状态
park与wait的区别:wait必须在synchronized里面,且唤醒的是随机,而park是消耗许可,unpark是颁发许可,可以提前unpark,park也会一次性消耗所有许可

总结

我们举一个例子:

		ReentrantLock reentrantLock = new ReentrantLock();new Thread(new Runnable() {@Overridepublic void run() {reentrantLock.lock();}}, "Thread-A").start();new Thread(new Runnable() {@Overridepublic void run() {reentrantLock.lock();}},"Thread-B").start();new Thread(new Runnable() {@Overridepublic void run() {reentrantLock.lock();}},"Thread-C").start();
  • 以上是三个线程尝试加锁,当然是只有第一个线程获取锁,调试结果如下,可以看到Thread-A即持有锁的线程在sync的属性里,而链表的头节点不记录线程信息但是状态为SIGNAL,而Thread-B的状态也为SINGAL,其后继节点Thread-C的状态就是默认状态0
    ![[Pasted image 20250121175854.png]]

AQS解锁逻辑

ReentrantLock.unlock

    public void unlock() {sync.release(1);}

AbstractQueuedSynchronizer#release

    public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}
  • 获取头节点,并调用头节点的unparkSuccessor,唤醒链表中的第二个节点

AbstractQueuedSynchronizer#unparkSuccessor

  private void unparkSuccessor(Node node) {int ws = node.waitStatus;if (ws < 0)node.compareAndSetWaitStatus(ws, 0);Node s = node.next;if (s == null || s.waitStatus > 0) {s = null;for (Node p = tail; p != node && p != null; p = p.prev)if (p.waitStatus <= 0)s = p;}if (s != null)LockSupport.unpark(s.thread);}
  • 先cas置换status
  • 获取后继节点,如果判断这个后继节点是null或者是cancel状态,说明该节点已经失效,那么就会从尾部开始向前找最接近头部的SINGAL节点或者状态是0的节点(那就是在队尾),这个很好理解,我们可以想象链表头节点往后的一段全是cancel,那么就是找到这一段cancel后的第一个singal节点并唤醒它,至于为什么从尾部开始向前找,是因为AQS指针连接的问题,这里就没有再深挖了

总结

我们再举一个例子,现在有四个线程,依次是1,2,3,4,其中只有2是cancel状态,1占有锁,当1释放锁后会是什么流程

  1. 根据解锁逻辑,会先找到3这个节点
  2. 此时unpark这个3节点,3节点回到acquireQueued这个方法里,进入第一个if:
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {  setHead(node); p.next = null;  // help GCreturn interrupted;
}

可以看到获取前驱节点,3的前驱是2,而2不是头节点,不会进入这个if,自然也不会尝试获取锁,所以会再次进入shouldParkAfterFailedAcquire

  • 当进入shouldParkAfterFailedAcquire,我们前面分析了它会从删除已经cancel的所有前驱节点,也就是说2节点会在这里面被移除了
  • 当2节点被移除后,此时再循环一次acquireQueued,这个时候3的前驱就是1节点也就是头节点了, 就可以正常获取锁了
http://www.lryc.cn/news/524523.html

相关文章:

  • 若依框架在企业中的应用调研
  • 【Day23 LeetCode】贪心算法题
  • 2025年PHP面试宝典,技术总结。
  • Qt中的按钮组:QPushButton、QToolButton、QRadioButton和QCheckBox使用方法(详细图文教程)
  • influxdb+grafana+jmeter
  • Net Core微服务入门全纪录(三)——Consul-服务注册与发现(下)
  • leetcode 479. 最大回文数乘积
  • 独立搭建UI自动化测试框架
  • 62,【2】 BUUCTF WEB [强网杯 2019]Upload1
  • Spring Boot 整合 ShedLock 处理定时任务重复执行的问题
  • 常见Arthas命令与实践
  • Glide加载gif遇到的几个坑
  • STM32学习之通用定时器
  • MiniMax-Text-01——模型详细解读与使用
  • Redis的Windows版本安装以及可视化工具
  • tensorflow源码编译在C++环境使用
  • 第四届机器学习、云计算与智能挖掘国际会议
  • #漏洞挖掘# 一文了解什么是Jenkins未授权访问!!!
  • QT QListWidget控件 全面详解
  • 【Vim Masterclass 笔记25】S10L45:Vim 多窗口的常用操作方法及相关注意事项
  • 包文件分析器 Webpack Bundle Analyzer
  • 代码随想录day14
  • react19新API之use()用法总结
  • 67,【7】buuctf web [HarekazeCTF2019]Avatar Uploader 2(未完成版)
  • ANSYS HFSS 中的相控天线阵列仿真方法
  • stm32 L051 adc配置及代码实例解析
  • KUKA示教器仿真软件OfficeLite8.6.2,EthernetKRL3.1.3通信
  • Erlang语言的并发编程
  • 【数据挖掘实战】 房价预测
  • 我的创作纪念日,纪念我的第512天