整体理解:
AQS为实现阻塞锁和相关的同步器(semaphores, events等)提供一个依赖于先进先出等待队列的框架。这个类为大部分依赖于一个单原子值(state)来表示状态的同步器提供了一个有用的基础。子类必须定义来改变这个状态值的protected方法,并且定义这个状态值的意义,用来获取或释放此类的对象。
它的子类们应该被定义为非公开的内部辅助类。并且被用来实现它们外部类的其它内部类(enclosing)的同步属性(就像ReentrantLock一样,一个SYC内部类实现了AQS的一些同步方法,并且其它的内部公平和非公平锁类调用了这个SYC类的一些方法来进行锁操作)。本类不实现任何同步接口,但它定义像这样acquireInterruptibly的能被具体的锁和相关的同步器执行的方法来实现它们的公共方法。
这个类不仅支持一个默认的独享模式,还支持一个共享模式。当前线程获取到独享模式后,其他线程如果尝试获取独享模式则会失败。不同的线程能够成功的获取到共享模式。在不同模式下等待的线程共享同一个FIFO队列。通常地,实现AQS的子类一般只支持这两个模式中的一个,但这两个模式可以同时起作用(ReadWriteLock)比如。只支持一个模式的子类只需实现一个模式的重写就行。
这个类维护者两个链式队列,一个是等待队列,另一个是Condition队列。
等待队列节点类:
等待队列是一个自旋锁(CLH)的变体。在这里我们用它来进行同步器的阻塞。在等待队列的每个节点中的waitstatus域追踪着一个线程是否需要阻塞。当一个线程的前任节点被释放后,它就会被唤醒。队列中每个节点被当作一个特定的监视器来持有一个等待线程。status域并不控制线程是否被加锁。当一个线程第一次进入等待队列中,它会尝试获取锁。但是作为第一个节点并不一定保证获取锁成功,它只是得到了竞争锁的权力。当前已释放锁的线程(如要重新获锁)需重新等待机会。
等待队列是一个双向链表结构。每个节点的prev域指向它的前任,并且prev的主要作用就是用来处理节点的取消(cancellation)。如果一个节点已经被取消,它的后继节点的prev应该链上一个非取消的前节点。
我们使用next域来实现阻塞技术。每个线程的id被保存在线程所属的节点中,所以一个节点的后继也就表示遍历下一个节点来判断它是哪个线程并且唤醒它。判断后继的过程必须要避免新线程将当前节点设置为它们的前任这个竞争过程的发生。
CLH队列的基本操作:尾插和头出
尾插:简单的原子操作,尾插就行了
头出:需要判断当前结点的前任,目地是为了处理可能的由于过时和中断造成的取消
CLH队列需要一个虚设的头结点来使用,但不会在队列构造时创建它们因为如果没有线程竞争这将会造成无谓的浪费。所以在第一次线程竞争的时候这个节点才会被构造。
Condition队列:
AQS中有一个Condition队列。在Condition队列中等待的线程使用和等待队列相同的节点,但需要一个额外的单向链表来构造这个Condition队列。Condition队列是一个单链表,因为只有在独享模式下Condition节点才会被访问。当节点进行await操作时,它会被插入condition队列。在执行singnal操作时,这个节点会被转变到等待队列中。有一个特定的值waitstatus=Condition用来标识这个节点到底是不是在Condition队列之中。
首先它是一个抽象类,意味着我们不能直接创建它的对象,我们需要构造一个能够去实现或继承它的类来使用它(也就是上述的:将它的实现类作为一个内部类来供其它类使用)
AQS中的节点类有以下属性:
/*等待队列类*/
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
/*表明线程由于过时或中断已被取消*/
static final int CANCELLED = 1;
/*表明后继结点已经或者将会被阻塞(通过park方法),并且当前结点在释放或取消时需要唤醒它*/
static final int SIGNAL = -1;
/*表明线程正在condition队列等待,直到它被转变到等待队列(status会被设置为0)之前,它不会被当做一个等待队列节点来使用*/
static final int CONDITION = -2;
/*一个共享模式的释放应该被传播到其他节点,表明下一个共享锁的获取应该无条件进行传播*/
static final int PROPAGATE = -3;
等待情形有这样几种:
1、SIGNAL -1
2、CANCELLED 1
3、CONDITION -2
4、PROPAGATE -3
5、0 初始状态
volatile int waitStatus;
/*双向链队*/
volatile Node prev;
volatile Node next;
/*当前线程*/
volatile Thread thread;
Node nextWaiter;
对于nextWaiter的解释:
在condition队列中等待的下一个节点,或者是特殊值SHARED。因为Condition队列只有在独享模式下才能被访问,所以当节点在Condition队列中等待时我们只需要一个单向的链表来存储它们。虽然每个节点有三个指针域,但Condition队列中的节点next域应该为空,只保留nextWaiter域与后继节点相连接
节点以外的属性:
private transient volatile Node head;
private transient volatile Node tail;
以上是等待队列头尾节点
private volatile int state;
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
state为同步状态标识默认值为0
get和set方法,这个state状态值会由AQS的继承类来创建方法并去改变,然后它们可以调用或重写AQS的get和set方法来取得或者替换改变后的state值
一些与等待队列相关的操作:
1、入队
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;
}
}
}
}
等待队列的入队操作为尾插,一个自旋,队列里有节点就直接插入
若没有就创建一个新的虚设的头结点,并在下一个循环中将节点插入到头节点后,然后return当前节点,跳出循环,一般会被addWatier调用
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;
}
如等待队列中有节点,就直接插入;若没有,就调用enq方法将此节点插入
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
将当前节点设置为队列的头节点,并将节点的thread和prev置空。仅会被aquire相关联的方法所调用
2、唤醒节点
/*唤醒当前节点的后继,如果存在的话。需要阻塞的节点通常都是当前节点的后继,但如果后继被取消或者是空,则从队列的最后一个节点往前搜,重新定位一个后继节点*/
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
/*首先若当前节点的waitStatus为-1,也就表明当前节点的后继需要唤醒,于是就利用原子操作将其waitStatus置为0*/
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*如果当前节点的后继为空,则跳出该方法
否则,从队列的最后一个节点一直往前,找到离当前节点最近的非取消节点,然后调用LockSupport的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);
}
流程:
1、判断waitStatus,为-1则表明当前节点的后继需要唤醒
2、将当前节点的waitStatus设置为0
3、从队列的最后一个节点一直往前,找到离当前节点最近的非取消节点,然后调用LockSupport的unpark方法将其阻塞
3、取消一个正在尝试获取资源的节点
private void cancelAcquire(Node node) {
/*如果节点不存在则忽略它*/
if (node == null)
return;
/*将当前节点的线程属性置空*/
node.thread = null;
Node pred = node.prev;
/*删除当前节点之前所有waitStatus大于0(已取消)的节点*/
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
/*将当前节点的waitStatus置为1(即取消当前节点)*/
node.waitStatus = Node.CANCELLED;
/*若当前节点为tail则直接删除,并将其前任设为tail*/
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
/*若前任节点不为头节点并且前任的后继需要被唤醒*/
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
/*如果当前节点后继不为空并且其waitstatus小于0则将其设为当前节点前任的后继(即删除当前节点)*/
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
/*否则唤醒当点节点的的后继来进行传播状态*/
} else {
unparkSuccessor(node);
}
/*将当前节点打结,帮助GC*/
node.next = node;
}
}
流程:
1、如果节点不存在则忽略它
2、遍历当前节点之前所有节点,删除已经取消的所有节点
3、将当前节点删除
4、阻塞等待队列中竞争失败的节点
/*当线程竞争失败就进入这个方法。如果这个线程应该阻塞,就返回true。这是在所有acquire调用链中最主要的信号控制方法*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*前任节点状态已为-1,此情况返回true
*/
return true;
if (ws > 0) {
/*当前线程的前任被取消,则一直往前搜,直到前任waitStatus不大于0,删除已经取消的节点,然后将当前线程链上
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*若前任waitStatus为0或-3,表明我们需要将后继其阻塞,但还未执行阻塞,所以利用原子操作将前任的waitStatus设为-1*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
/*其余情况返回false*/
return false;
}
流程:
1、节点竞争资源失败进入此方法。若当前节点的前任状态已为-1,此情况返回true
2、否则删除当前节点之前所有已经取消的节点
3、若前任waitStatus为0或-3,表明我们需要将后继其阻塞,但还未执行阻塞,所以将前任的waitStatus设为-1
总之这个方法并不能将节点阻塞,它只是改变了节点的waitStatus,标识这节点可以被阻塞。正真的阻塞方法还得由acquireQueued来调用
/*阻塞并核查中断信号*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
5、在等待队列中尝试获取资源:
/*独享非中断模式下已经存在于队列中的节点尝试获取资源*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
/*自旋操作*/
for (;;) {
/*获取当前节点的前任节点*/
final Node p = node.predecessor();
/*若前任是头节点并且调用tryAcquire方法成功获取,则将当前节点设为头节点,并释放前任,然后跳出循环,返回中断标记*/
if (p == head && tryAcquire(arg)) {
setHead(node);
/*帮助GC*/
p.next = null;
failed = false;
return interrupted;
}
/*如果上面if方法获取失败,首先调用shouldParkAfterFailedAcquire方法
若返回true则证明当前节点需要阻塞,并调用上述parkAndCheckInterrupt方法将当前线程阻塞,然后核查中断,
接着核对中断信息,若当前线程被中断,则将中断标记置为true并停止自旋操作;
若返回false则阻塞失败,再次进入自旋操作中尝试获取资源*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这个acquire方法是针对独享模式的
流程:
1、进入自旋操作尝试获取资源
2、若前任是头节点并且调用tryAcquire方法成功获取,则将当前节点设为头节点,并释放前任,然后跳出循环
3、若获取失败,则调用shouldParkAfterFailedAcquire方法判断它是否要被阻塞
4、要么当前节点被阻塞,要么阻塞失败,重新进行自旋操作
6、尝试获取独享模式
public final void acquire(int arg) {
/*1、如果tryAcquire方法获取成功则返回true,&&左边为则为false于是直接返回
2、如果失败则调用addWaiter方法将节点插入等待队列并将其标记为独享模式,然后调用acquireQueued方法*/
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
尝试获取独享模式,忽略中断。通过执行至少一次tryAcquire方法来实现这个方法,若成功,返回true。否则当线程进队后,可能会重复阻塞或解阻,一直执行tryAcquire方法直到成功。这个方法可以用来实现Lock方法
7、在独享模式下释放资源
public final boolean release(int arg) {
/*调用tryRelease方法,来唤醒当前队列中头节点的后继节点*/
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
/*头节点为空或者头节点waitStatus为0*/
return false;
}
8、判断节点是否为队列的头节点
/*判断当前节点是否是队列中的第一个节点,是就返回false,不是则返回true*/
public final boolean hasQueuedPredecessors() {
/* 1、若当前队列为空,则h==t,返回false
2、若当前队列只有一个节点,则返回false
3、否则若当前线程所属节点不为第二个节点,返回true;若等于,返回false*/
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
9、一些会被子类重写的方法:
/*尝试获取独享模式,会被Acquire等方法调用去获取资源*/
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
/*尝试反映在独享模式下的一个释放过程,会被tryRelease方法调用*/
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
/*判断当前线程是否拥有独享模式*/
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
10、判断节点是否在等待队列中
/*判断一个节点,一般是一个初始化在condition队列中的节点,现在是否正在尝试重新获取进入sync(等待)队列中的资格, 若是返回true*/
final boolean isOnSyncQueue(Node node) {
/*若节点的waitStatus 为-2,即它处于等待队列中或者它是Condition队列中头节点时,返回false*/
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
/*若next域有值,则证明它肯定在等待队列中,因为condition队列中next域已被nextWaiter域所代替*/
if (node.next != null)
return true;
return findNodeFromTail(node);
}
与Condition队列有关的方法:
11、在等待队列中寻找指点的节点
/*若节点在等待队列中,则从后往前搜找到指定的节点*/
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
12、将Condition队列中节点插入到等待队列中:
/*用来改变节点的waitStatus*/
final boolean transferForSignal(Node node) {
/*将节点的waitStatus变为0,失败则返回false,若操作成功则往下执行。*/
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;
}
13、完全释放资源(包括重入的锁)
/*释放独享锁(包括重入锁)*/
final int fullyRelease(Node node) {
boolean failed = true;
try {
/*获取state值*/
int savedState = getState();
/*调用AQS中的release方法,然后release会调用ReentrantLock中重写的tryRelease方法尝试释放锁*/
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
上面略过了一些与中断和共享模式有关的实现方法,这些方法的实现在此暂不研究