目录
一、什么是AQS ?
AQS(AbstractQueuedSynchronizer),正如其名,抽象的队列同步器,它提供了一套可用于实现锁同步机制的框架,是除了java自带的synchronized关键字之外的锁机制,许多同步类的实现都依赖于它,如:CountDownLatch、Semaphore、ReentrantLock...
二、AQS框架
它维护了一个volatile int State(代表资源,可以通过getState()、setState()、CompareAndSetState()操作这个变量)和一个CLH队列(它是一个FIFO的双向队列),队列的每一个Node节点用于包装线程。线程请求共享资源时,如果被请求的资源state是空闲的,线程就获取资源,得以继续执行,否则将线程阻塞并放入CLH同步队列中,等待被唤醒继续争夺资源。
AQS
根据资源互斥级别提供了独占(Exclusive)和共享(Share)两种资源访问模式。
独占:资源只能被一个线程使用。如:ReentrantLock是一个独占式的锁。
共享:资源同时能被多个线程所使用。如:Semaphore、CountDownLatch。
AQS是一个典型的模板模式,我们在自定义同步器时只需要继承它并实现其中的共享资源state的获取和释放的方法即可,AQS已经定义好主要的功能、模块及调用流程。前面提到了AQS的两种资源共享方式(独占和共享),所以需要实现它的方法也有两套(根据选择的共享方式,实现其中的一套即可;也可以同时实现独占和共享两种方式,如ReentrantReadWriteLock)。
独占方式:
boolean tryAcquire(int): 尝试获取资源。传入的参数为获取资源的个数。如果成功返回true,否则返回false。
boolean tryRelease(int): 尝试释放资源。传入的参数为释放资源的个数。如果成功返回true,否则返回false。
共享方式:
int tryAcquireShared(int): 尝试获取资源。0和正数代表获取 成功,负数则获取失败。
boolean tryReleaseShared(int): 尝试释放资源。传入的参数为释放资源的个数。成功返回true,失败返回false。
以ReentrantLock为例,它实现的是tryAcquire/tryRelease, state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的.
再以Semaphore为例,它实现了tryAcquireShared/tryReleaseShared,state>0表示有可用的资源数,state=0资源不可用,多个线程获取资源时会自旋CAS ,让 state = state - 请求的资源个数,当 state < 0时,线程资源获取失败放入CLH同步队列中,如果 state>0则该资源还能分配给其他线程。
三、Node
Node
主要包含5个核心字段:
waitStatus:当前节点状态,该字段共有5种取值:
-
CANCELLED = 1
。表示当前结点已取消调度。当节点引用线程由于等待超时或被打断时会进入此状态。 -
SIGNAL = -1
。表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。 -
CONDITION = -2
。当节点线程进入condition
队列时的状态。 -
PROPAGATE = -3
。共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。 -
0
。节点初始化时的状态。
prev:前驱节点。
next:后继节点。
thread:引用线程,头节点不包含线程。
nextWaiter:
condition`条件队列。
注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常
四、源码讲解
1.1 acquire
acquire
核心为tryAcquire
、addWaiter
和acquireQueued
三个函数,其中tryAcquire
需具体类实现。 每当线程调用acquire
时都首先会调用tryAcquire
,失败后才会挂载到队列,因此acquire
实现默认为非公平锁。
public final void acquire(int arg) {
//tryAcquire(arg)尝试获取资源(自定义),获取失败执行下一步addWaiter(Node.EXCLUSIVE)将线程加入同步队列中。
// acquireQueued(node,arg)在线程节点加入队列后判断是否可再次尝试获取资源,并返回获取结果
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt(); //等待过程中被中断,补上中断
}
addWaiter
将线程包装为独占节点,尾插式加入到队列中,如队列为空,则会添加一个空的头节点。值得注意的是addWaiter
中的enq
方法,通过CAS+自旋
的方式处理尾节点添加冲突。
private Node addWaiter(Node mode) { //将当前线程加入同步队列
//以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
Node node = new Node(Thread.currentThread(), mode); //创建当前线程的Node节点
//尝试快速方式直接放到队尾。
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也指向它
if (compareAndSetHead(new Node()))
tail = head;
} else { //队列不为空,又执行之前那一步
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueue
在线程节点加入队列后判断是否可再次尝试获取资源,如不能获取则寻找安全点,然后park进入阻塞状态,直到线程被unpark。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; //标记是否成功拿到资源
try {
boolean interrupted = false; //标记在等待过程中是否被中断过
for (;;) {
final Node p = node.predecessor(); //获取当前node的前置节点
if (p == head && tryAcquire(arg)) { //如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
setHead(node); //将头节点设置为当前节点
p.next = null; // help GC
failed = false; //成功获取到资源
return interrupted; //返回
}
//未拿到资源,park进入阻塞状态,等着被unpark()或interrupt()
//shouldParkAfterFailedAcquire(p,node) : 找到安全点,成功返回true
//parkAndCheckInterrupt() 阻塞并检测阻塞时是否被中断过
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true; //阻塞被中断,true
}
} finally {
if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire检查状态,看是否可以去休息了,否则将其前驱节点标志为SIGNAL
状态(表示其需要被unpark
唤醒)后,再通过park
进入阻塞状态。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; //拿到前驱节点的状态
//SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
if (ws == Node.SIGNAL) //如果前驱节点的状态为Node.SIGNAL
/*
*这个节点已经设置了前置节点为SIGNAL,即前置节点在释放资源时会唤醒它,所以它是一个安全点,就可以放心休息了
*
*/
return true;
if (ws > 0) { //前置节点已经被取消
/*
*
* 前置进程已经被取消,跳过这个进程,从该节点往前找一个安全点
*/
do {
node.prev = pred = pred.prev; //不断往前找,直到遇到正常的节点
} while (pred.waitStatus > 0);
pred.next = node; //让这个状态正常的节点指向当前节点
} else {
/*
*
* pred.waitStatus设置为SIGNAL,但是不会park,等下一轮
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt如果线程找好安全休息点后,那就可以安心去休息了。此方法就是让线程去休息,真正进入等待状态。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//调用park()使线程进入waiting状态
return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
}
1.2 Release
release流程较为简单,尝试释放成功后,即从头结点开始唤醒其后继节点,如后继节点被取消,则转为从尾部开始找阻塞的节点将其唤醒。阻塞节点被唤醒后,即进入acquireQueued
中的for(;;)
循环开始新一轮的资源竞争。
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唤醒当前线程的下一个线程。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus; //获取当前线程节点状态
if (ws < 0) //如果小于零
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next; //获取下一个节点
if (s == null || s.waitStatus > 0) { //为空或者已经被取消CANCELLED
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) //从队尾往前找
if (t.waitStatus <= 0) //从这里可以看出,waitStatus<=0的结点,都是还有效的结点。
s = t; //记录有效节点
}
if (s != null)
LockSupport.unpark(s.thread); //唤醒线程
}
2.1 acquireShared
acquireShared和releaseShared
整体流程与独占锁类似,tryAcquireShared
获取失败后以Node.SHARED
挂载到队尾阻塞,直到队头节点将其唤醒。在doAcquireShared
与独占锁不同的是,由于共享锁是可以被多个线程获取的,因此在首个阻塞节点被唤醒后,会通过setHeadAndPropagate
传递唤醒后续的阻塞节点。
public final void acquireShared(int arg) { //共享式获取资源
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg); //获取失败,将当前线程加入队列,直到获取到资源为止才返回。
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
doAcquireShared它与acquireQueued非常相似,是不过将补中断selfInterrupt()放到了里面,唯一不同的是setHeadAndPropagate(node, r)
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) { //如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
int r = tryAcquireShared(arg);
if (r >= 0) { //资源获取成功
setHeadAndPropagate(node, r); //将head指向自己,还有剩余资源可以再唤醒之后的线程
p.next = null; // help GC 前驱节点被垃圾回收
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//找安全点,park,等着被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
setHeadAndPropagate将头节点指向自己,并且检查资源是否还有剩余,如果有调用doReleaseShared继续唤醒其余阻塞的节点。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node); //将头节点指向自己
//如果还有剩余量,继续唤醒下一个邻居线程
//如果propagate > 0,表示存在多个共享锁可以获取,可直接进行doReleaseShared唤醒阻塞节点
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
doReleaseShared 用于唤醒后继节点线程,这里没看懂也没关系,知道它的作用就行了。
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) { //队列不为空且有后继
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h); //唤醒后继节点
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // CAS失败继续循环
}
if (h == head) //head被改变
break;
}
}
2.2 releaseShared
releaseShared它会尝试释放指定的资源,如果释放成功则继续尝试调用doReleaseShared去唤醒同步队列里的线程来获取资源。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared(); //唤醒后继节点
return true;
}
return false;
}
小结:
到这里AQS的几个核心方法也讲完了,相信到了这里大家对AQS的框架也有了基本的认识,来对前面的知识做一下总结吧。
AQS是一个抽象的类,它已经定义好了主要的功能、模块及调用流程。AQS
根据资源互斥级别提供了独占(Exclusive)和共享(Share)两种资源访问模式,其中的tryAcquire/tryRelease或tryAcquireShared/tryReleaseShared需代码要自行定义。
独占锁共享锁默认都是非公平获取策略,可能被插队。也可以自定义tryAcquire/tryRelease或tryAcquireShared/tryReleaseShared方法让它成为公平锁。独占锁只有一个线程可获取,其他线程均被阻塞在队列中;共享锁可以有多个线程获取。独占锁释放仅唤醒一个阻塞节点,共享锁可以根据可用数量,一次唤醒多个阻塞节点。希望大家一定要搞清楚独占锁和共享锁的区别!!!