如果你恰巧翻到了这篇文章,建议可以看这两篇文章,写的很好。Java并发之AQS详解,(JDK)ReetrantLock手撕AQS。
前言
锁和AQS的关系
在讲队列同步器AbtstractQueuedSynchronizer(AQS,下文简称为同步器)之前,先了解锁和它的关系。AQS是实现锁或者说很多同步组件的关键,我们可以从源码中看到很多类继承了AQS,例如重入锁,读写锁等等。换句话说,锁是面向使用者,而同步器是实现锁的关键组件。
同步器的接口
同步器的设计是基于模板方法模式的,使用者(也就是上面截图中的这些类,例如FairSync,NonFireSync等)需要继承并重写指定的方法,随后将重写的方法组装到自定义的同步组件实现中。
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
以上5个方法为子类可以重写的方法,摘抄自Java并发之AQS详解。除了可以重写的方法,同步队列AQS中会提供一些模板方法,这些方法可以直接被其子类调用来实现自定义同步组件。例如独占是获取同步状态的acquire(),独占式释放同步状态release()等等。本文只分析acquire()和release()的相关方法。
acquire()方法
这是AQS的最重要的方法之一
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
此方法是独占式获取同步状态,如果当前线程获取同步状态成功,则返回,如果不成功,将会进行同步队列等待。那么如何具体实现呢?
- tryAcquire(int)独占式获取同步状态,如果当前线程获取同步状态成功,则返回。
- 如果不成功,使用addWaiter(Node)方法将当前线程构造成节点加入到同步队列尾部。
- 最后调用acquireQueued(Node, int)方法使得该节点以“死循环”的方式获得同步状态。如果获得不到,则阻塞当前节点中的线程,那么被阻塞谁来唤醒呢,唤醒是由前驱节点出队列或者阻塞线程被中断来实现。
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断
tryAcquire(int arg)
这个方法就是用来独占式的获取当前线程的同步状态,如果成功返回true,失败返回false,但是这个方法体中怎么直接抛出异常了呢?什么鬼?这就是上面提到的同步器需要继承自己的子类所要实现的方法。说白了就是谁要用谁自己实现,AQS只是提供一个框架和一些必要的方法。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
addWaiter(Node mode)
这个方法是将当前线程构造为节点,添加到等待队列尾部。
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;
}
直接看代码, 这里稍微提一下Node这个静态内部类的数据结构以及属性。
Node
AQS在判断状态时,通过用waitStatus>0表示取消状态,而waitStatus<0表示有效状态。
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;
// 这个是和Condittion有关,
static final int CONDITION = -2;
// 表示下一次共享式同步状态将会无条件的传播下去
static final int PROPAGATE = -3;
// 等待状态,初始状态为0,其余状态就是上面提到的1,-1等等
volatile int waitStatus;
// 节点的前节点
volatile Node prev;
// 节点的后继节点
volatile Node next;
// 线程值
volatile Thread thread;
// 等待队列中的后继节点。如果当前节点时共享的,那么该字段是SHARED常量
Node nextWaiter;
...
}
接着我们看enq()方法
enq(final Node node)
enq方法将并行添加的节点请求通过CAS变得串行化,实现节点的队尾插入。
private Node enq(final Node node) {
// “死循环”将node插入队尾
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 通过CAS将节点放入队尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
接着我们来看acquireQueued(final Node node, int arg)方法
acquireQueued(final Node node, int arg)
到了这一步说明获取同步状态失败,当前线程被构造成节点后已经添加到队列尾部,进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,通过for的死循环,把等待队列中的节点状态不是大于0的无效状态的节点的节点状态置为SIGNAL,让它们进入WAITING状态,而只有当头结点获得同步状态后移除同步队列,将后续节点设置为头结点,继续进行同步状态的获取。换句话说就是等待队列中的节点除了头结点在获取同步状态,其余的除了节点状态大于0的都被park,都处于WAITING状态。
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节点已经获得同步状态,将后节点设置为null时是为了垃圾回收
p.next = null;
failed = false;
// 返回是否被打断
return interrupted;
}
// 线程阻塞,需要其他线程进行唤醒
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
}
} finally {
if (failed)
// 如果出现异常或者出现中断,将线程的状态改为CANCELLED
cancelAcquire(node);
}
}
在acquireQueued(final Node node, int arg)方法中还主要涉及一下两个方法,继续来看
shouldParkAfterFailedAcquire(Node, Node)
这个方法主要是检查和更新节点状态,如果线程阻塞则返回true,就是将不能够获得同步状态的节点都让他们等待,除非被唤醒或者打断。
// 检查和更新未能获取的节点的状态。如果线程阻塞,返回true。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;// 拿到前驱节点状态
if (ws == Node.SIGNAL)
// 如果前驱节点状态为SIGNAL,表示前趋结点会通知它,那么它可以放心大胆地挂起了
return true;
if (ws > 0) {
// 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果前驱正常,那就把前驱的状态设置成SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt()
这个方法就是让线程进行等待,park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。
// 如果被打断返回true
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//调用park()使线程进入waiting状态
return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
}
acquireQueued(final Node node, int arg)方法总结,重要!
- 该方法传入一个node节点,先判断这个节点是不是头结点,也就是说如果当前队列是空队列,现在就只有这么一个节点(其实还有一个null节点,它的后序节点就是刚添加进来的这个节点),如果是头结点,那么调用自定义同步器重写的tryAcquire()方法进行独占式同步队列的获取。如果获取失败继续获取,这是一个for的死循环。那么如果不是空队列,添加到这个同步队列中,假设队列中已经有5个节点,那么现在这个刚添加的节点进入此方法的for死循环,进行判断是它的前驱节点是否为头结点,明显不是,因为队列中它的前面还有5个节点。
- 既然前驱节点不是头结点,那么进入阻塞方法shouldParkAfterFailedAcquire(Node, Node)方法,它会节点判断是否要让这个节点中的线程阻塞,传入的第一个参数是当前节点的前驱节点,第二个参数是当前节点,如果前驱节点的状态是SIGNAL,那么就返回true,接着让接下来的parkAndCheckInterrupt()方法真正进行park,如果状态>0,那么就跳过,找状态不大于0的有效状态,如果既不是SIGNAL,也不是>0的情况,那么就将前驱节点的状态置位SIGNAL,其实正常情况下,按照假设的情况,它是第六个节点,那么它前面的节点状态要么是无效状态,要么都已经置为SIGNAL,除了头结点正在获取同步状态,剩下的节点中的线程都处于WATTING状态。因为这里是for的死循环,假设的这个节点是第六个节点,for循环一次,要么第五个节点已经是SIGNAL,要么给它置为SIGANL,如果不是SIGANL,这次置为SIGANL后,那么下一次添加了第七个节点,直接判断得出已经是SIGANL了,已经进入阻塞状态了,不用再进行CAS进行SIGNAL信号的赋值了。
- 既然已经阻塞了,那么进行了parkAndCheckInterrupt()方法体,直接将当前线程park,让其进入等待状态。直到被unpark()或interrupt()唤醒。
- 在唤醒后,继续进入for循环判断自己是否有条件获取同步状态,如果有,那么将后序节点置为头结点,并返回自己是否被打断过。如果不能获得同步状态,那么继续进入阻塞状态。
require()方法总结
总结一下独占式获得同步状态的过程:
- 调用自定义同步器重写的tryAcquire()方法来独占式获取同步状态,如果获取成功则返回。
- 如果失败,将当前线程构造为节点,调用addWaiter()方法添加到等待队列尾部。
- acquireQueued()方法,使得当前节点的线程排队等待,处于WATTING状态,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
说完了独占式获取同步状态,再说一下独占式释放线程
release(int arg)
public final boolean release(int arg) {
if (tryRelease(arg)) {
// 找到头结点
Node h = head;
// 头结点不为null,而且头结点状态不是初始值0,也就是其他节点可以获取同步状态
if (h != null && h.waitStatus != 0)
// 唤醒node的后继节点(如果存在的话)。
unparkSuccessor(h);
return true;
}
return false;
}
在这个方法中我们可以看到是通过tryRelease(arg)的返回值来判断是否将同步状态释放,这个方法也是自定义同步器需要继承重写的方法,代码如下
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
接着我们可以看到还有个unparkSuccessor(Node node)方法,它唤醒队列中等待的下一个线程
unparkSuccessor(Node node)
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;//拿到当前节点状态
if (ws < 0)
// 置零当前节点状态,可能会失败
compareAndSetWaitStatus(node, ws, 0);
// 后继节点
Node s = node.next;
// 如果为null或者已取消
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);// 唤醒线程
}
这个方法核心就是unpark唤醒了队列中最前面等待的线程。
参考文献
[1]https://www.cnblogs.com/waterystone/p/4920797.html
[2]https://www.jianshu.com/p/e4301229f59e