前几天在对比Synchronized和ReentrantLock的关系和区别时,以及学习使用Semaphore、CountDownLatch和CyclicBarrier时,发现里面底层都有这样一个同步器。这让我觉得学习它们的底层原理,就不得不学习AQS自身的底层原理,那么,我们就来吧。
这里参考了队列同步器(AQS)详解 和 这才是图文并茂:我写了1万多字,就是为了让你了解AQS是怎么运行的
目录
成员变量
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 7373984972572414691L;
/**
创建一个新的AbstractQueuedSynchronizer实例,初始同步状态为零。
*/
protected AbstractQueuedSynchronizer() { }
static final class Node{...} //数据结构中的节点,下面细说。
/**
等待队列的头部,延迟初始化。 除初始化外,仅通过 setHead 方法进行修改。 注意:如果 head 存在,则保证其 waitStatus 不会被 CANCELLED。
*/
private transient volatile Node head;
/**
等待队列的尾部,延迟初始化。 仅通过方法 enq 修改以添加新的等待节点
*/
private transient volatile Node tail;
/**
同步状态。
*/
private volatile int state;
}
可以看出同步器本身其实就是一个双向链表(行为约束上,更像一个双端队列),我们直接能获取的节点只有head和tail。主要是我们要考察这个节点Node的含义。其中head节点为正在运行线程的的节点。
Node
static final class Node {
/** 指示节点在共享模式下等待的标记 */
static final Node SHARED = new Node();
/** 指示节点正在以独占模式等待的标记 */
static final Node EXCLUSIVE = null;
/** 指示线程已取消的 waitStatus 值*/
static final int CANCELLED = 1;
/** 指示后继节点需要被唤醒的waitStatus 值 */
static final int SIGNAL = -1;
/** waitStatus 值指示线程正在等待条件(进入等待队列) */
static final int CONDITION = -2;
/**指示下一个acquireShared 应无条件传播的waitStatus 值*/
static final int PROPAGATE = -3;
volatile int waitStatus;
/**
前驱节点
*/
volatile Node prev;
/**
后继节点
*/
volatile Node next;
/**
该节点装载的线程 在构造时初始化并在使用后归零。
*/
volatile Thread thread;
/**
链接到下一个等待条件的节点,或特殊值 SHARED。 因为条件队列只有在独占模式下才会被访问,所以我们只需要一个简单的链接队列来保存节点,因为它们正在等待条件。 然后将它们转移到队列以重新获取。 并且因为条件只能是独占的,所以我们通过使用特殊值来表示共享模式来保存字段。
*/
Node nextWaiter;
/**
如果节点在共享模式下等待,则返回 true。
*/
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;
}
}
代码里定义了一个表示当前Node节点等待状态的字段waitStatus,取值总共有CANCELLED(-1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、0,这5个值代表了不同的特定场景。
- CANCELLED:表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
- SIGNAL:表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL(记住这个-1的值,因为后面我们讲的时候经常会提到)
- CONDITION:表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。(注:Condition是AQS的一个组件,后面会细说)
- PROPAGATE:共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
- 0:新结点入队时的默认状态。
当waitStatus为负值表示结点处于有效等待状态,为正值的时候表示结点已被取消。
独占式同步组件的设计
同步器提供的模板方法
//独占式获取同步状态,如果当前线程获取同步状态成功,立即返回。否则,将会进入同步队列等待,
//该方法将会重复调用重写的tryAcquire(int arg)方法
public final void acquire(int arg) {...}
//与acquire(int arg)基本相同,但是该方法响应中断。
public final void acquireInterruptibly(int arg){...}
//独占式释放同步状态,该方法会在释放同步状态后,将同步队列中第一个节点包含的线程唤醒
public final boolean release(int arg) {...}
供子类重写的方法
//独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态
protected boolean tryAcquire(int arg)
//独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
protected boolean tryRelease(int arg)
//当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占
protected boolean isHeldExclusively()
具体方法实现
acquire方法
/**
以独占模式获取,忽略中断。 通过至少调用一次tryAcquire ,成功返回。 否则线程会排队,可能会反复阻塞和解除阻塞,调用tryAcquire直到成功。 此方法可用于实现方法Lock.lock 。
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) && //如果tryacquire失败
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//生成节点并加入同步队列
selfInterrupt();
}
/**
为当前线程和给定mode创建节点并排入队列
*/
private Node addWaiter(Node mode) { //共享/独占性,null为独占
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) { //
node.prev = pred;//连接到尾部之后
if (compareAndSetTail(pred, node)) { //CAS设置,期望当前尾部是pred,是的话改为node。
//如果CAS尝试成功,就说明"设置当前节点node的前驱"与"CAS设置tail"之间没有别的线程设置tail 成功
//只需要将"之前的tail"的后继节点指向node即可
pred.next = node;
return node;
}
}
enq(node);//否则,通过死循环来保证节点的正确添加
return node;
}
/**
每个节点以死循环方式来判断自身是否头结点。
*/
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; //自旋退出
}
if (shouldParkAfterFailedAcquire(p, node) && //获取同步状态失败后判断是否需要阻塞或中断
parkAndCheckInterrupt()) //阻塞当前线程
interrupted = true;
}
} finally {
if (failed) //获取锁失败,则将此线程对应的node的waitStatus改为CANCEL
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 前驱结点等待状态为"SIGNAL",那么自己就可以安心等待被唤醒了
return true;
if (ws > 0) {
/*
* 前驱结点被取消了,通过循环一直往前找,直到找到等待状态有效的结点(等待状态值小于等于0) ,
* 然后排在他们的后边,至于那些被当前Node强制"靠后"的结点,因为已经被取消了,也没有引用链,
* 就等着被GC了
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果前驱正常,那就把前驱的状态设置成SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
上述逻辑可由下图表示:
当前线程获取同步状态失败时,同步器会将当前线程、等待状态等信息构造成一个Node并加入同步器队列中,同时会阻塞当前线程。
同时 acquireQueued 会进行如下操作:
1、CAS自旋,先判断当前传入的Node的前结点是否为head结点,是的话就尝试获取锁,获取锁成功的话就把当前结点置为head,之前的head置为null(方便GC),然后返回
2、如果前驱结点不是head或者加锁失败的话,就调用shouldParkAfterFailedAcquire
,将前驱节点的waitStatus变为了SIGNAL=-1,最后执行parkAndChecknIterrupt
方法,调用LockSupport.park()
挂起当前线程,parkAndCheckInterrupt
在挂起线程后会判断线程是否被中断,如果被中断的话,就会重新跑acquireQueued
方法的CAS自旋操作,直到获取资源。
ps:LockSupport.park方法会让当前线程进入waitting状态,在这种状态下,线程被唤醒的情况有两种,一是被unpark(),二是被interrupt(),所以,如果是第二种情况的话,需要返回被中断的标志,然后在acquire
顶层方法的窗口那里自我中断补上
可以看到节点和节点之间在循环检查的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点,这样就使得节点的释放规则符合FIFO。并且也便于对过早通知的处理(过早通知是指:前驱节点不是头节点的线程由于中断而被唤醒)。
Release
head节点的线程在释放同步状态时,将会唤醒后续节点,后续节点将在获取同步状态成功时将自己设置为head节点。
public final boolean release(int arg) {
if (tryRelease(arg)) { //释放同步状态成功的话
Node h = head;
if (h != null && h.waitStatus != 0) //独占即为SIGNAL -1 要唤醒后面的
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus; //当前节点waitStatus
if (ws < 0)
//将head结点的状态置为0
compareAndSetWaitStatus(node, ws, 0);//更新为0
//找到下一个需要唤醒的结点s
Node s = node.next;
//如果为空或已取消
if (s == null || s.waitStatus > 0) {
s = null;
// 从后向前,直到找到等待状态小于0的结点,前面说了,结点waitStatus小于0时才有效
for (Node t = tail; t != null && t != node; t = t.prev) //从尾部往前找
if (t.waitStatus <= 0)
s = t;
}
// 找到有效的结点,直接唤醒
if (s != null)
LockSupport.unpark(s.thread); //唤醒
}
方法逻辑就是,先将head的节点状态置为0,避免下面找节点的时候再找到head,然后找到队列中最前面的有效节点,然后唤醒,我们假设这个时候线程A已经释放锁,那么此时队列中排最前边竞争锁的线程B就会被唤醒。