一、概述
AQS的全称为(AbstractQueuedSynchronizer),我们知道的java.util.concurrent包下很多类如,ReetrantLock ,CountDownLactch(闭锁),Semaphore(信号量)都是基于AQS架构上构建的。因此笔者的理解AQS是Lock实现的前提。
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
**注意:AQS是自旋锁:**在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功
实现了AQS的锁有:自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是AQS的衍生物
二、数据结构
state表示共享的资源数量,而列表是一个FIFO队列(当多个线程发生竞争资源被阻塞时,会进入这个队列,当同步状态释放的时候,会把首节点唤醒,使其尝试获取同步状态)
三、主要特点
- state由volatile修饰,对此值的修改需要使用Unsafe包下的CAS操作对其修改。
- 内部阻塞与唤醒,通过LockSupport类来解决这个问题,park实现了阻塞当前线程,unpark唤醒被阻塞的线程。
- 除了上图中的同步队列以外还有一个Condition队列(但不是必须包括,如果涉及Condition会使用)。同步队列是一个双向链表,包含head和tail节点。Condition队列是一个单向链表。
- 支持共享和独占两种模式,不同的自定义同步器竞争共享资源的方式不同。
- 例如ReetrantLock是一个独占锁,state初始化为0,表示无锁状态,一旦某个线程获取到对象锁会独占此锁,并将state+1,其他线程会获取资源失败,直到该线程释放资源为止。
- 再以CountDownLatch为例,如果有一个任务想要往下执行,但必须等待多个子任务执行完后再继续往下执行。那么CountDownLatch就会初始化n个任务准备执行,state为任务的个数,任务执行完一个state通过CAS减1,直到任务执行完state减为0,共享模式同一时刻state可以提供给多个线程使用。
四、内部类结构和方法
4.1 Node
Node 表示等待线程的节点
static final class Node {
/** 只存在共享锁模式 和 独占锁模式 */
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
/*见下文分析*/
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
/** 为上面4个值其中1个*/
volatile int waitStatus;
/** 当前节点的前置节点*/
volatile Node prev;
/** 当前节点的后置节点*/
volatile Node next;
/** 当前节点所关联的线程*/
volatile Thread thread;
/**指向下一个在某个条件上等待的节点或者指向 SHARE 节点,当前处于共享模式*/
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
- CANCELED:1 表示节点被取消 由中断或者超时造成。此时的节点不会去再去转化其他状态,常在cancelAcquire被设置
- SIGNAL:-1 表明当前节点的后继结点正在或者将要被阻塞,因此当前节点被释放或者取消时需要唤醒其后继节点
- CONDITION:-2 表示节点在CONDITON队列中
- PROPAGATE:-3 一个传播唤醒操作,因为共享锁模式中有多个资源,表明后续的后继节点都会被唤醒。(只有头节点才能设置为该状态)
4.2 acquire(int)
方法流程
源码分析
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- 首先尝试获取tryAcquire(具体实现由子类完成)主要对state进行操作,通过 compareAndSet保证多线程下只有一个线程能够修改成功,修改成功则返回true
- 如果没有成功则addWaiter,将当前线程处理为NODE加入同步队列中
- 通过acquireQueued方法处理同步队列中等待的线程,将队列中首个线程循环尝试获取资源,直到获取到资源则返回。其他非首节点进入阻塞状态,等待被唤醒(unpark()或者interrupt())
- 获取到资源后进行selfInterrupt 。
接下来详细分析上述的几个方法
4.2.1 tryAcquire(int)
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
该方法尝试去获取独占资源,如果获取成功,则返回true,反之返回false,该方法的具体实现有子类实现,比如ReetrantLock通过该方法实现了锁是否可重入。
4.2.2 addWaiter(Node)
private Node addWaiter(Node mode) {
//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方式
enq(node);
return node;
}
private Node enq(final Node node) {
//cas 自旋加入队尾
for (;;) {
Node t = tail;
if (t == null) {
//队尾为空,则创建一个head节点并将tail指向head
if (compareAndSetHead(new Node()))
tail = head;
} else {
//放入队尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- 尝试在队尾添加
- 如果存在尾节点
(1)将节点的前驱节点指向尾节点(node.prev = tail)
(2)通过CAS操作将当前节点设置为尾节点(tail=node)
(3)如果2成功则将之前的尾节点的后继节点指向当前节点(pred.next=node)
- 队尾添加失败或者尾节点为空(enq方法)
(1)如果尾节点说明队列还没有初始化,所以也通过CAS分配头节点并将尾节点指向头节点
(2)重复1的步骤将节点设置在尾节点后
4.2.3.1 acquireQueued(Node,int)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();(1)
if (p == head && tryAcquire(arg)) {(2)
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())(3)
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
当节点进入等待队列后,会先检查当前节点的前驱节点是否为head,如果是head会自旋请求资源,如果不是则调用shouldParkAfterFailedAcquire判断是否应该挂起当前节点
(1) 获取当前节点的前驱节点
(2)如果前驱节点是头节点,并且能够获取锁的资源那么当前节点就能获取锁 将head指向该节点。
(3)继续进入等待状态
4.2.3.2 shouldParkAfterFailedAcquire(Node,Node)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//如果前驱节点的状态尾SIGNAL就将当前节点线程阻塞
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
//如果ws大于0表明前驱线程为CANCELED所以需要遍历出不为CANCELED节点为止
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//CAS控制如果前驱节点正常就把前驱节点设置为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
表明当前节点是否需要被阻塞(LockSupport.park),如果前驱节点是SIGNAL则可以将当前节点阻塞,如果通过操作找到一个前驱为SINGNAL。最后如果确定了可以阻塞当前节点的线程,就会掉用parkAndCheckInterrupt进行线程阻塞
4.2.3.3 parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
4.3 release(int)
方法流程
占有了资源就会对应释放资源,如果彻底释放则state=0。方法流程如下
源码分析
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来唤醒后继节点
4.3.1 unparkSuccessor(Node)
private void unparkSuccessor(Node node) {
//利用CAS将头节点状态设置为0,表示后继节点已经被唤醒了
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//如果头节点为null或者为CANCELED。则从尾部开始找离头部最近的需要唤醒的节点,然后unpark唤醒
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);
}
看完了独占锁的介绍我这里打个比喻,我们使用AQS就如同去食堂打饭,但是食堂只有一个打饭阿姨。所以第一来到阿姨面前的同事,阿姨可以给这个同事打饭。所以这个同事就是head节点,而观察阿姨是否能给自己打饭的过程就是tryAcquire。而后面的同事呢发现阿姨正在给别人打饭所以tryAcquire失败就会在打饭的同事后面排队,排队的过程就是addWaiter。而由于大家到了饭点了俄着呢,就期盼着快点打饭好吃饭,尤其是我们这个正在打饭同事的后面这个同事,由于马上就可以打饭了心里美滋滋的,所以一直看到底能不能给自己打饭。这个过程就是我们自旋获取state的过程。而再后面的同学呢,由于知道还早着呢,但是饿着了没力气,所以就发着呆等着前面的同学打完,一旦前面的同学打完一个饭,就会吆喝着自己饭打完了,而剩下的同学就会被提醒,就会看看自己的前面同学是不是在打饭了,如果是就会判断是不是马上该自己打饭了,如果不是则继续发呆。这整个过程就是获取与释放独占锁的过程,下面介绍下共享锁。
4.4 acquireShared(int)
方法流程
源码分析
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- 尝试获取tryAcquireShared如果大于等于0证明能够获取共享资源
- doAcquireShared有一部分和独占锁类似就是只有前驱节点为head的节点才能去竞争锁。如果该节点获取到了共享锁,会执行setHeadAndPropagate方法唤醒其他线程去获取锁,这也是共享锁多个线程能共享资源的因素。
4.4.1 setHeadAndPropagate
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);(1)
if (propagate > 0 || h == null || h.waitStatus < 0) {(2)
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
(1) 移除头节点,并将当前节点设置为头节点,因而其他节点的头节点改变,其他被唤醒的节点有机会去获取锁。
(2)判断是否需要唤醒后继节点 propagate表示剩余的资源,只有节点的后继节点不为独占模式时才能去唤醒后继节点
4.4.2 doReleaseShared
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {(1)
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);(2)
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;(3)
}
if (h == head)
break;
}
}
(1)如果head=null说明没有初始化,head=tail说明队列没有等待队列
(2)如果head状态为SIGNAL说明后继节点需要唤醒则CAS将SIGNAL状态修改为0表明当前节点已经唤醒了后继节点从而调用上文所述的
unparkSuccessoru 唤醒节点
(3)节点状态为0说明不需要唤醒后继节点,则将后继节点修改为PROPAGATE状态失败则继续重新循环继续操作
共享锁与独占锁的区别在于独占锁只有释放了资源以后才能唤醒等待的线程,而在共享模式下获取锁与释放锁都有可能释放等待的线程。
4.5 releaseShared(int)
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
入参arg表示释放的资源数,而tryReleaseShared仍然由于子类实现。如果释放成功则掉用doReleaseShared唤醒其他线程,此方法已经在上文介绍到。
链接:https://www.jianshu.com/p/6529403e37d8