AQS原理
文章目录
一、AQS简介
AQS是一个抽象队列同步器,是一个抽象类,他定义了一个模板,具体是由各个子类实现的,像ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore这些常用的实现类都是基于AQS实现的。AQS的实现依赖于内部的同步队列,它是一个FIFO双向队列,如果当前线程获取同步状态失败,AQS会将该线程以及等待状态等信息构造成一个Node,将其加入同步队列的尾部,同时阻塞当前线程,当同步状态释放时,唤醒队列的头节点。AQS具备特征:
- 阻塞等待队列
- 共享/独占
- 公平/非公平
- 可重入
- 允许中断
1、AQS内部的方法属性
(1)AQS内部维护属性volatile int state (32位)
state表示资源的可用状态
(2)State三种访问方式
getState()、setState()、compareAndSetState()
(3)AQS定义两种资源共享方式
Exclusive-独占,只有一个线程能执行,如ReentrantLockShare-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
(4)AQS定义两种队列
同步等待队列
条件等待队列
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共
(5)享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/
唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去
实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回
false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回
false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成
功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续
等待结点返回true,否则返回false
2、获取同步的状态
假设线程A要获取同步状态,初始状态state=0,线程A可以顺利获取锁,A获取锁后将state置为1。在A没有释放锁期间,线程B来获取锁,此时因为state=1,锁被占用,所以将B的线程信息和等待状态等数据构成一个Node节点,放入同步队列中,head和tail分别指向队列头部和尾部(此时队列中有一个空的Node节点作为头点,head指向这个空节点,空Node的后继节点是B对应的Node节点,tail指向它),同时阻塞线程B,阻塞使用的是LockSupport.park()方法。后续如果还有线程要获取锁,都会加入队列尾部并阻塞。
3.释放同步的状态
当线程A释放锁时,将state置为0,此时线程A会唤醒头节点的后继节点,唤醒其实是调用LockSupport.unpark(B)方法,即线程B从LockSupport.park()方法返回,此时线程B发现state已为0,所以线程B可以顺利获取锁,线程B获取锁后,B的Node节点出队。
二、reentrantLock源码分析
1.非公平锁实现原理
(1)加锁解锁流程
先从构造器开始看,默认为非公平锁实现
NonfairSync 继承自 AQS
没有竞争时
第一个竞争出现时
Thread-1 执行了
- CAS 尝试将 state 由 0 改为 1,结果失败
- 进入 tryAcquire 逻辑,这时 state 已经是1,结果仍然失败
- 接下来进入 addWaiter 逻辑,构造 Node 队列
- 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
- Node 的创建是懒惰的
- 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程
当前线程进入 acquireQueued 逻辑
- acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
- 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
- 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false。
shouldParkAfterFailedAcquire方法是判断一个争用锁的线程是否应该被阻塞。它首先判断一个节点的前置节点的状态是否为Node.SIGNAL,如果是,是说明此节点已经将状态设置,如果锁释放则应当通知它,所以它可以安全的阻塞,返回true。
如果前节点的状态大于0,为CANCELLED状态时,则从前节点开始循环找到一个没有被CANCELLED节点设置为当前节点的前节点,返回false。在下次循环执行shouldParkAfterFailedAcquire时,返回true。这里是把队列中CANCELLED的节点删除掉。
如果shouldParkAfterFailedAcquire返回了true,则会执行parkAndCheckInterrupt()方法,它是通过LockSupport.park(this)将当前线程挂起到WATING状态,它需要等待一个中断、unpark方法来唤醒它。
4.shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时
state 仍为 1,失败
- 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回true
- 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)
再次有多个线程经历上述过程竞争失败,变成这个样子
Thread-0 释放锁,进入 tryRelease 流程,如果成功
设置 exclusiveOwnerThread 为 null
state = 0
当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程
找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1
回到 Thread-1 的 acquireQueued 流程
如果加锁成功(没有竞争),会设置 - exclusiveOwnerThread 为 Thread-1,state = 1
- head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
- 原本的 head 因为从链表断开,而可被垃圾回收
如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了
如果不巧又被 Thread-4 占了先
Thread-4 被设置为 exclusiveOwnerThread,state = 1
Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞
(2)可重入原理
当以一个线程过来后就会首先去获取他的状态,如果为0就会尝试去加锁,如果已经获取了锁就会判断是否是是当前线程的,如果是当前线程就证明发生了锁重入,每一次获取到此线程的锁状态就会加一,解锁时只有防state减为1才会释放锁。
static final class NonfairSync extends Sync {
// ...
// Sync 继承过来的方法, 方便阅读, 放在此处
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()) {
// state++
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryRelease(int releases) {
// state--
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 支持锁重入, 只有 state 减为 0, 才释放成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
}
2、公平锁实现原理
公平锁与非公平锁的区别在于tryAcquire 方法的实现,公平锁方法里面获取到获取到状态后首先会检查队列中是是否有前驱节点,没有前驱节点才会去竞争。有前驱节点就会尝试去唤醒这个线程去获取锁。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquire(int arg) {
if (
!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
) {
selfInterrupt();
}
}
// 与非公平锁主要区别在于 tryAcquire 方法的实现
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 先检查 AQS 队列中是否有前驱节点, 没有才去竞争
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;
}
// ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// h != t 时表示队列中有 Node
return h != t &&
(
// (s = h.next) == null 表示队列中还有没有老二
(s = h.next) == null ||
);
}
}