用途
AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。 决定线程是否能够使用共享资源。
使用到的设计模式
模板设计模式
设计整体思路
多个线程在访问共享资源前,首先先经过AQS判断当前同步器的状态,是否可以获取资源,而这个判断的规则由 我们开发者来定义。如ReetrantLock定义state状态大于0则代表有线程已经占用。对状态判断后的入队,唤醒操作,aqs已经实现好。 总结:开发者使用aqs的时候,只需要关注state的实际含义。
源码解读
类属性和方法
属性:
- state:用来判断当前是否获取到资源,对于每个实现AQS方法的工具而言,定义不同
- head:记录队列的头指针
- tail:记录队列的尾指针
需要子类实现的抽象方法(对state状态的判断)
- boolean tryAcquire(独占式获取资源)
- boolean tryRelease (独占式释放资源)
- int tryAcquireShared (共享式获取资源)
- int tryReleaseShared (共享式释放资源)
- boolean isHeldExclusively (是否独占)
什么是独占:资源只能由一个线程使用 什么是共享:资源可以由多个线程同时使用
注意:独占式获取的返回参数,是boolean类型,而共享式返回对象为int类型。即独占式只需要对状态判断返回false代表获取失败,共享式通过state值小于0代表获取失败。
接下来源码解读以ReetrantLock为例子。
判断是否可以获取锁(由开发者实现)
首先ReetrantLock是独占式,且可重入的锁。 什么是可重入?如下代码:
public class ReentranLockTest implements Runnable{
ReentrantLock lock = new ReentrantLock();
public void get() {
lock.lock();
try{
set();
}
catch(Exception e){
}
finally{
lock.unlock();
}
}
public void set() {
lock.lock();
try{
// do something
}
catch(Exception e){
}
finally{
lock.unlock();
}
}
@Override
public void run() {
get();
}
public static void main(String[] args) {
ReentranLockTest ss = new ReentranLockTest();
new Thread(ss).start();
}
}
线程调用get()方法,获取一次锁,get()中调用set()还会再一次获取锁,此时该线程已经拥有了锁,还可以再次同样的锁,就是可重入。 ReetrantLock有公平和非公平获取锁方式,这是题外话,不影响对aqs的理解,以默认的非公平的实现方式为例。
// reetrantLock中这个方法就类似 tryAcquire 是默认调用的方法
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 状态值为0,说明没有人获取,则可以获取资源
if (compareAndSetState(0, acquires)) {
// 记录当前线程为锁对象拥有者
setExclusiveOwnerThread(current);
return true;
}
}
// 不为0,说明已经名花有主,这个时候判断是否是本身线程,实现可重入功能
else if (current == getExclusiveOwnerThread()) {
// 重入的时候,state就累加1,释放的时候state就减一
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// state不为0,且当前资源拥有者也不是当前线程,则获取失败,由aqs将该线程放入等待队列
return false;
}
tryAcquire方法,由开发者实现锁的时候定义,实现的过程主要关注的是state的状态,和当前资源的拥有者。
根据state的判断结果,阻塞线程
这段代码的关键点就是,tryAcquire负责判断线程是否有资格可以获取资源,有则设定锁对象拥有者,不存在则返回fasle,进入等待队列的操作。而这一部分的操作,aqs已经帮我们实现好,无需开发者实现。这个公共方法的入口如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
该方法可以说是获取锁的核心逻辑入口:
-
判断是否获取到锁 tryAcquire(获取锁失败,到这个方法返回值为false,继续执行后面的方法)
-
没有获取到锁,加入等待队列addWaiter
-
加入队列后,暂停线程acquireQueued(使用LockSupport.park阻塞当前获取失败的线程)
加入等待队列
首先这个等待队列有几个特点:
-
队列是双向列表结构
-
先进先出
-
队列的首节点是哨兵节点。
什么是哨兵节点? 刚开始的时候,队列中的head,tail指针为null。当有线程进入以后,会执行两次循环,一次创建哨兵节点,一次将失败的线程包装成Node加入队列中。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 尾巴指针对象不为空,说明不是刚开始,已经有线程存在
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 第一个线程进入的时候,会在循环里执行两次,
// 第一次创建哨兵节点,第二次循环创建自己
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
图示过程:
为什么需要哨兵节点? 反证法,如果没有哨兵节点的话,双向队列,如何出队列呢?
// 无哨兵出队列
// 获取当前头结点
Node pre = head;
// 获取下一个节点,重新设置头结点为下一个节点
Node next = head = pre.next;
if(next != null){
// 下个节点的前置节点设置为null
next.pre = null;
}else{
// 头节点下一个节点为null,说明队列为空,重新设置tail
tail = null;
}
// 方便gc
pre.next = null;
// 有哨兵出队列(node是当前需要出的节点)
// 获取头节点(此时是哨兵节点)
Node pre = head;
// 重新设置头结点为此时要出队的线程的节点
head = node;
// 原先存储线程的节点,变成哨兵节点
node.thread = null;
node.pre = null;
// 方便回收旧哨兵节点
pre.next = null;
有哨兵节点出队列如图: 首先代码上:减少了一次判空操作。 其次,并发场景下,如果没有哨兵节点,假设只有一个节点要出队列的情况,要同时把 head 和tail 变成null。如果这个时候才把head变成null了,同时发生入队,此时tail还没变null,造成数据错乱。 使用哨兵节点的情况,tail只负责入队操作,head只负责出队操作,职能更单一。不存在只剩最后一个节点出队时要同时维护head和tail数据为null的可能。
加入队列后,自旋判断是否有机会出队,无则阻塞
出队列的条件:前驱节点为头结点 阻塞前的条件:前驱节点状态设置为-1
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 阻塞前,再次判断是否此时有没有可能获得到资源的控制权
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 不满足出队列条件,判断是否可以暂停线程
// 线程暂停的条件:前驱节点状态为-1
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 说明线程被打断过
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
// 注意,LockSupport.park(this);若此时线程被其他线程打断,会虚假唤醒
LockSupport.park(this);
// 故需要重新设置打断标识,此时被打断会返回true,且清除标记位
return Thread.interrupted();
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 前驱节点的状态为-1
if (ws == Node.SIGNAL)
return true;
// 前驱节点状态大于0说明前驱节点被其他线程取消了
// 向前遍历没被取消的节点,挂载到该节点后面,且设置前驱节点状态为-1
// 如果是这种被取消的情况,会执行shouldParkAfterFailedAcquire 3次,由外层循环控制
// 第一重新挂载,第二次设置前驱节点状态为-1,第三次判断前驱节点状态为-1返回true
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
总结:
释放资源
释放资源后,需要做2件事,修改state状态,唤醒等待队列中的线程。 解锁步骤aqs的入口为
public final boolean release(int arg) {
// 这个tryRelease和上面tryAcquire一样,需要开发者对state状态判断和修改
if (tryRelease(arg)) {
Node h = head;
// 结合进入队列,在线程暂停之前,会把前面的,非取消的线程的节点状态设置为-1,
// 被取消的节点状态是为1
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
和加锁一样tryRelease(arg)由各个锁对state状态判断是否具备解锁条件,以reetrantLock为例
protected final boolean tryRelease(int releases) {
// 获取state,计算
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 为0 代表可以解锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
解锁需要,改变state状态(tryRelease方法实现),唤醒等待队列中的线程(unparkSuccessor实现)
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 前驱节点为哨兵节点,所以要唤醒的是哨兵的后一个节点,且状态不为取消(取消的状态值为1)
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);
}
条件队列,阻塞
该方法作用与synchronized中wait作用相同。 条件变量的唤醒,将当前队列加入等待条件中
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 加入等待条件队列
// 阻塞队列与获取锁的队列不同,
// 没有哨兵节点,因为此时已经获取到锁了,不存在竞争条件
Node node = addConditionWaiter();
// 释放锁
int savedState = fullyRelease(node);
int interruptMode = 0;
// 节点中的线程阻塞
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
// 若中途被打断,则结束循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
private Node addConditionWaiter() {
Node t = lastWaiter;
// 清理条件队列中,被取消的线程
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
条件队列,唤醒
该方法作用与synchronized中notify/notifyAll作用相同。
public final void signal() {
// 判断当前线程是否是有资格唤醒,只有持有锁才能释放条件队列
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将阻塞队列中,加入前前一个节点设置状态为-1,改为-1就有责任去唤醒自己的后继节点
参考资料
blog.csdn.net/m0_37989980… cloud.tencent.com/developer/a… concurrent.redspider.group/article/02/…