AQS
墙裂推荐:Java并发之AQS详解:
https://www.cnblogs.com/waterystone/p/4920797.html
AbstractQueuedSynchronizer, 一个用来构建锁和同步器的框架。
ReentrantLock,Semaphore, FutureTask都是基于AQS来构建的。
独占模式线程acquire流程
此方法是独占模式下的线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,知道获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。
源码:
// AQS
public abstract class AbstractQueuedSynchronizer{
public final void acquire(int arg) {
// 1. 尝试获取资源
if (!tryAcquire(arg) &&
// 2. 将线程加入等待队列尾部,并标记为独占模式
// 3. 是线程阻塞在等待队列中获取资源。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 4. 等待过程中若出现中断,在获取资源后再进行中断处理。
selfInterrupt();
}
// 1.尝试获取独占资源。
// 具体的资源获取、释放方式交给自定义的同步器去实现。
// 非abstract方法,独占模式只需要实现tryAcquire和tryRelease。共享模式只需要实现tryAcquireShared和tryReleaseShared。减少了不必要的实现。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
// 2.加入等待队列
private Node addWaiter(Node mode) {
// 将线程封装为节点。mode为EXCLUSIVE 或 SHARED
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)) {
pred.next = node;
return node;
}
}
// 队列中没有元素,通过enq入队
enq(node);
return node;
}
private Node enq(final Node node) {
// CAS自旋等待,直到成功加入队尾
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 队列为空则创建一个空的结点作为head,将node插入在head之后。
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 【指向前一个】
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
// 3.加入等待队列中的线程进入休息(停放)状态,直到其它线程释放资源后通知自己。
final boolean acquireQueued(final Node node, int arg) {
// 未拿到资源
boolean failed = true;
try {
// 等待过程中是否被中断
boolean interrupted = false;
// 自旋
for (;;) {
// 前驱结点
final Node p = node.predecessor();
// 前驱为head,该结点可获取资源。且已经获取到资源。
// head要么为null,要么为当前获取到资源的那个结点。
if (p == head && tryAcquire(arg)) {
// 将node置为head结点的状态。prev=null。
setHead(node);
// prev和next都为null了,方便GC回收。意味着拿过资源的结点出队。
p.next = null; // help GC
// 已拿到资源
failed = false;
// 返回等待过程中是否被中断
return interrupted;
}
// 如果自己可以休息,通过park()进入waiting状态,直到被unpark().
// 如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 拿到前驱状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 该节点已经设置了状态,要求释放时发出信号,因此后继结点可以安全地停放。
return true;
if (ws > 0) {
// 前驱结点是取消状态(>0),跳过此结点,继续向前寻找。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
// 找到一个正常的前驱结点。
pred.next = node;
} else {
// 前驱正常,将其设为signall,释放资源后通知自己。有可能失败,也许前驱结点刚释放完。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
// 调用park方法,使线程进入waiting状态。
// 等待unpack()方法或者interrupt()唤醒自己。
LockSupport.park(this);
// 如果被唤醒,查看自己是不是被中断的。
return Thread.interrupted();
}
// 4. 中断方法
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
}
流程总结:
- 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
- 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
- acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
独占模式线程release流程
此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(state=0),它会唤醒等待里的其它线程来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock()。
源码:
public abstract class AbstractQueuedSynchronizer{
public final boolean release(int arg) {
// 1.释放资源
// 根据tryRelease的返回值来判断线程是否已经完成资源的释放。
// 在自定义同步器在设计tryRelease时候需要注意该返回值。
if (tryRelease(arg)) {
// 2.找到头结点
Node h = head;
if (h != null && h.waitStatus != 0)
// 3.唤醒等待队列里的下一个线程
unparkSuccessor(h);
return true;
}
return false;
}
// 1. 释放资源,需要在自定义的同步器中去实现。
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
// 2. 当前结点置为0状态,唤醒等待队列中的下一个线程
private void unparkSuccessor(Node node) {
// node为当前线程,状态为负数,则需要清除等待信号。
int ws = node.waitStatus;
// 小于0则状态置0.大于0则为取消状态线程
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 找到后继结点
Node s = node.next;
// 没有后继结点 或者 后继结点是取消状态
if (s == null || s.waitStatus > 0) {
s = null;
// 从后往前找,直到当前结点的下一个有效结点。
for (Node t = tail; t != null && t != node; t = t.prev)
// 小于等于0的都是有效的结点
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒上述过程中找到的下一个有效线程
LockSupport.unpark(s.thread);
}
}
为什么要从后往前找?
有两种异常情况,会导致next链不一致:
1)s==null,在新结点入队时可能会出现
2)s.waitStatus > 0,中间有节点取消时会出现(如超时)
总之,由于并发问题,addWaiter()入队操作和cancelAcquire()取消排队操作都会造成next链的不一致,而prev链是强一致的,所以这时从后往前找是最安全的。
为什么prev链是强一致的?因为addWaiter()里每次compareAndSetTail(pred, node)之前都有node.prev = pred,即使compareAndSetTail失败,enq()会反复尝试,直到成功。一旦compareAndSetTail成功,该node.prev就成功挂在之前的tail结点上了,而且是唯一的,这时其他新结点的prev只能尝试往新tail结点上挂。这里的组合用法非常巧妙,能保证CAS之前的prev链强一致,但不能保证CAS后的next链强一致。
共享模式线程acquireShared流程
此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。
public abstract class AbstractQueuedSynchronizer{
public final void acquireShared(int arg) {
// 1. 获取资源
if (tryAcquireShared(arg) < 0)
// 2.将当前线程加入队列尾部
doAcquireShared(arg);
}
// 1. 尝试获取资源
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
// 2.将当前线程加入队列尾部
private void doAcquireShared(int arg) {
// 加入队列尾部
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
// 前驱
final Node p = node.predecessor();
// 如果前一个是head,当前线程有被唤醒有可能是head用完资源来唤醒自己的
if (p == head) {
// 尝试获取资源
int r = tryAcquireShared(arg);
// 获取成功
if (r >= 0) {
// 将head指向自己
setHeadAndPropagate(node, r);
// prev和next都设置为null了
p.next = null; // help GC
if (interrupted)
// 等待过程被中断过,此时才执行中断流程
selfInterrupt();
// 已获取资源
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
// 如果还有剩余量,继续唤醒下一个有效线程
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
}
-
- tryAcquireShared()尝试获取资源,成功则直接返回;
- 失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。
其实跟acquire()的流程大同小异,只不过多了个自己拿到资源后,还会去唤醒后继队友的操作(这才是共享嘛)。
共享模式线程releaseShared流程
public abstract class AbstractQueuedSynchronizer{
public final boolean releaseShared(int arg) {
// 1. 尝试释放资源
if (tryReleaseShared(arg)) {
// 2. 唤醒后续结点
doReleaseShared();
return true;
}
return false;
}
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
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; // loop to recheck cases
// 唤醒后继
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
}
此方法的流程也比较简单,一句话:释放掉资源后,唤醒后继。跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。例如,资源总量是13,A(5)和B(7)分别获取到资源并发运行,C(4)来时只剩1个资源就需要等待。A在运行过程中释放掉2个资源量,然后tryReleaseShared(2)返回true唤醒C,C一看只有3个仍不够继续等待;随后B又释放2个,tryReleaseShared(2)返回true唤醒C,C一看有5个够自己用了,然后C就可以跟A和B一起运行。而ReentrantReadWriteLock读锁的tryReleaseShared()只有在完全释放掉资源(state=0)才返回true,所以自定义同步器可以根据需要决定tryReleaseShared()的返回值。
acquire()和acquireShared()两种方法下,线程在等待队列中都是忽略中断的。AQS也支持响应中断的,acquireInterruptibly()/acquireSharedInterruptibly()即是,相应的源码跟acquire()和acquireShared()差不多