一文读懂 AQS 的前世今生
一、AQS 是什么?
AQS 本质就是 JUC 包下的一个抽象类 AbstractQueuedSynchronizer。AQS 本身不提供并发下的一个具体的功能,都是 JUC 包的一些工具类,锁之类的内容,需要基于 AQS 去实现。
更多的 AQS 是作为一个基础类,让其他的类去继承。
AQS 中大致有三个核心内容
state 属性: 本质就是一个 int 类型,但是这个属性及其重要。比如 ReentrantLock,如果 state 为 0,代表没有线程持有当前锁资源。如果 state >0 代表某个线程在持有当前锁资源。
同步队列(双向链表):一个由 Node 对象组成的同步队列。如果某个线程获取锁资源失败了,当前线程需要排队等一会,这个同步队列就是排队等待的地儿。
双向链表
单向链表: 也是由 Node 对象组成的一个单向链表。比如持有锁的线程,执行了 await 方法,当前线程会封装为 Node,添加到这个单向链表中。
单向链表
二、AQS 核心底层和 Lock 是什么关系?
ReentrantLock 意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁。为了帮助大家更好地理解 ReentrantLock 的特性,我们先将 ReentrantLock 跟常用的 Synchronized 进行比较,其特性如下:
ReentrantLock 就是借助了 AQS 实现的锁机制。只不过 ReentrantLock 类没有直接去的继承 AQS,而是他的内部类 Sync 去继承的 AQS,实现的一些具体的业务逻辑。
锁的一些逻辑是由抽象类 Sync 实现的,一些特有的功能,比如和公平以及非公平锁先关的内容是 Sync 的子类
NonfairSync: 非公平锁
FairSync: 公平锁
使用 ReentrantLock 可以指定是公平锁不是非公平锁,可以通过 ReentrantLock 提供的有参构造来决定使用哪种方式,默认情况下,使用的是非公平锁。如果参数传递为 true,采用公平锁。
优先分析一下 lock 方法的逻辑。
发现 ReentrantLock 执行 lock 方法后,会直接调用 sync 的 lock 方法。但是 sync 没有直接实现 lock 方法的逻辑,是由 NonfairSync 和 FairSync 去实现的具体逻辑。
先看 NonfairSync:
加锁总结:
通过 ReentrantLock 的加锁方法 Lock 进行加锁操作。
会调用到内部类 Sync 的 Lock 方法,由于 Sync#lock 是抽象方法,根据 ReentrantLock 初始化选择的公平锁和非公平锁,执行相关内部类的 Lock 方法,本质上都会执行 AQS 的 Acquire 方法。
AQS 的 Acquire 方法会执行 tryAcquire 方法,但是由于 tryAcquire 需要自定义同步器实现,因此执行了 ReentrantLock 中的 tryAcquire 方法,由于 ReentrantLock 是通过公平锁和非公平锁内部类实现的 tryAcquire 方法,因此会根据锁类型不同,执行不同的 tryAcquire。
tryAcquire 是获取锁逻辑,获取失败后,会执行框架 AQS 的后续逻辑,跟 ReentrantLock 自定义同步器无关。
解锁总结:
通过 ReentrantLock 的解锁方法 Unlock 进行解锁。
Unlock 会调用内部类 Sync 的 Release 方法,该方法继承于 AQS。
Release 中会调用 tryRelease 方法,tryRelease 需要自定义同步器实现,tryRelease 只在 ReentrantLock 中的 Sync 实现,因此可以看出,释放锁的过程,并不区分是否为公平锁。
释放成功后,所有处理由 AQS 框架完成,与自定义同步器无关。
三、AQS 如何尝试获取资源?
发现 tryAcquire 在 AQS 中并没有具体的实现逻辑,需要子类自己去实现在 ReentrantLock 中,就提供了两种实现的机制:
非公平锁源码实现
公平锁源码实现
四、AQS 获取资源失败如何排队?
当线程基于 tryAcquire 获取锁资源失败后,需要执行 addWaiter 方法,去 AQS 的同步队列中排队。这里的逻辑确保 AQS 同步队列初始化有监控节点后,将当前节点插入到最后面~~~不存在失败的情况,死循环插不成功就一直尝试。源码如下:
如果扔到同步队列失败,基于 eng 保证可以去排队 AQS 的同步队列,在头部的节点是一个伪/监控的节点在 AQS 的同步队列还没有初始化时,先初始化一个 Node,作为监控节点然后再将当前获取锁资源失败的线程排到最后面。
主要的流程总结如下:
通过当前的线程和锁模式新建一个节点。
Pred 指针指向尾节点 Tail。
将 New 中 Node 的 Prev 指针指向 Pred。
通过 compareAndSetTail 方法,完成尾节点的设置。这个方法主要是对 tailOffset 和 Expect 进行比较,如果 tailOffset 的 Node 和 Expect 的 Node 地址是相同的,那么设置 Tail 的值为 Update 的值。
五、AQS 排队后如何重新尝试获取资源?
前面获取锁资源失败了,并且已经基于 addWaiter 扔到同步队列排队之后,要走当前逻辑 acquireQueued 方法如果 Node 节点是排在 head.next 位置的,直接尝试抢锁如果没抢到,或者是不是排在 head.next 位置的。
确保自己能够被唤醒
挂起线程 (当释放锁后,如果 head 节点的状态是-1,就会唤醒后续节点)
版权声明: 本文为 InfoQ 作者【是月月啊2023】的原创文章。
原文链接:【http://xie.infoq.cn/article/0fcfd55b15e8c696463f220bd】。文章转载请联系作者。
评论