在JUC原理一篇了解了JUC的都是围绕着AbstractQueuedSynchronizer(简称AQS)实现的,也就是说AbstractQueuedSynchronizer为JUC的核心,本篇就是探索AbstractQueuedSynchronizer的源码。
AQS的实现原理
在开始阅读源码之前再来梳理一下JUC的原理,那么在阅读源码时可以提高效率。
AQS的两个队列
在AQS中存在两个队列用于对线程的状态保存和记录,分别是同步队列和条件队列。
同步队列中的线程可以处于独占和共享两种状态,并且通过队列是一个双端队列。其结构如下:
而条件队列中的线程状态必须为独占模式,线程在获取锁时不满足条件,则将线程添加至条件队列并等待唤醒。当条件队列中的线程被唤醒后,将线程从条件队列中剔除并添加至同步队列而等待执行,条件队列是单向链表结构,结构如下:
在AQS中,实现队列的是通过AbstractQueuedSynchronizer类中的一个静态内部类Node实现的:
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
}
}
具体的字段和方法分析在下面详细解释。
AQS的工作流程
AQS的工作机制流程如下:
这个的流程是独占锁的工作流程,流程如下:
- 首先线程获取锁,当线程获取锁失败,将当前线程添加至同步队列中;
- 将当前线程放入同步队列中后,线程仍然是通过for循环自旋转进行锁的争夺(不是直接挂起,类似于sychronized的自旋锁优化)。
- 在自旋转过程中,当前线程还是会争抢锁(可能存在当前线程还没挂起持有锁的线程已经执行完毕),同时会将当前线程在同步队列中的前一个节点状态修改为SIGNAL(该状态用于表示节点需要被唤醒)。
- 在自旋转过程中既没有获取锁,同时也将当前线程在同步队列中的前置节点状态修改为SIGNAL,则将当前线程阻塞。
- 当获取锁的线程执行完毕后,则从同步队列中从头节点开始向后遍历,唤醒第一个阻塞的线程。
- 线程被唤醒后,继续从步骤2继续执行,如此反复知道获取锁。
- 线程获取到锁后并执行完代码则唤醒队列中的线程。
AQS的原理机制及流程介绍完了,接下来来分析AQS相关的源码。当然本篇暂时不介绍条件队列的流程。
AQS的队列实现Node
首先从AQS中实现同步队列和条件队列的Node类说起。首先来介绍下其相关的属性及作用。
-
独占和共享的标识。
/** Marker to indicate a node is waiting in shared mode */ static final Node SHARED = new Node(); /** Marker to indicate a node is waiting in exclusive mode */ static final Node EXCLUSIVE = null;
锁分为独占锁和同步锁,而这两个标识用于表示线程是独占模式还是共享模式。
-
标识线程的状态。
static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3;
CANCELLED :值为1。当线程等待超时或者被中断,则取消等待,设等待状态 为-1,进入取消状态则不再变化。
SIGNAL : 值为-1。后继节点处于等待状态,当前节点(为-1)被取消或者中断 时会通知后继节点,使后继节点的线程得以运行。
CONDITION值为-2。当前节点处于等待队列,节点线程等待在Condition上,当 其他线程对condition执行signall方法时,等待队列转移到同步队列,加入到对同步状态的获取。
PROPAGATE:值为-3。与共享模式相关,在共享模式中,该状态标识结点的线程处 于可运行状态。 -
指定线程的等待状态。
volatile int waitStatus;
-
实现队列的前置指针和后置指针。
volatile Node prev; volatile Node next;
-
节点持有的线程。
volatile Thread thread;
-
条件队列单向指针。
Node nextWaiter;
当然这个属性有两个用途,在条件队列中该属性为指向下一个节点的指针,当在同步同列中用于表示当前节点的线程是独占模式还是共享模式。
-
判断当前线程是独占还是共享。
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类的属性和方法相对来说没有那么复杂。接下来来梳理AbstractQueuedSynchronizer的主要方法,这里主要是独占锁相关的方法(共享锁的相关方法大同小异)。
AQS的核心属性和方法
这里梳理的属性和方法均是独占锁相关的属性和方法,同时并没有对可中断和指定时间等待方法做讲解,当然一下方法熟悉后那些功能也容易理解多了。
-
记录同步队列中头节点和尾节点属性。
private transient volatile Node head; private transient volatile Node tail;
等待队列的头节点,懒加载模式,除去初始化,只能在setHead中修改
注意:如果头结点存在,它的状态waitSattus不能为CANCELLED,即1。
等待队列的尾部,懒加载模式,只能在enq方法中新增加等待节点时修改。 -
资源状态。
同步状态,用于统计获取锁的次数。 -
状态获取和修改方法。
protected final int getState() { return state; } protected final void setState(int newState) { state = newState; }
-
CAS设置state状态。
protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
-
节点入队方法。
private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
将参数节点追加到同步队列中,该方法返回队列中原尾部节点。
1>如果尾部节点为空,则初始化头部和尾部节点,新建一个Node实例(该Node实例 的waitStatus为int默认值为0),将head和tail指向该Node实例,然后将参数节点追加到新建的队列中。
2>如果尾部节点不为空,队列存在,则将参数节点追加到队列中。
该方法通过循环和CAS算法保证数据正确 -
向同步队列中增加节点。
private Node addWaiter(Node mode) { 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(node); return node; }
1>根据参数mode常见指定模式的节点。
2>判断tail尾部节点是否问空,不为空尝试添加至同步队列,成功则返回新建的节点。
3>不成功,则调用enq方法入队。
4>返回新建节点。 -
设置头节点。
private void setHead(Node node) { head = node; node.thread = null; node.prev = null; }
将head指向参数节点,并将参数节点的线程置为空,将参数节点前置节点置为空。该方法是用在独占锁获取到锁之后,将获取到锁的当前节点这是为头节点。
-
唤醒后置节点。
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) { 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>参数节点的waitSatus<0,则将其waitSatus修改为0。
2>判断参数节点的后置节点waitStatus是否是<=0,如果是则唤醒。
3>如果不是,则从同步队列中由尾部向前遍历,查找最靠前的满足thread不为空并且 waitStatus<=0的几点,将其线程唤醒。
该方法用于在锁资源释放后然后调用该方法唤醒后续的线程。 -
取消抢占资源。
private void cancelAcquire(Node node) { if (node == null) return; node.thread = null; Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev; Node predNext = pred.next; node.waitStatus = Node.CANCELLED; 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; if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next); } else { unparkSuccessor(node); } node.next = node; // help GC } }
1>忽略空节点。
2>参数节点线程置为空。
3>由给定节点向前遍历找到第一一个没有被取消的节点(pred)。
4>如果参数节点为tail,则将tail指向pred并将pred指向null结束。
5>如果参数节点不是tail,判断pred不是头节点,并且pred的状态为SIGNAL或者修改SIGNAL成功,则将参数节点后的节点追加到pred后,如果不满足则唤醒后续节点。
主要就是将队列中的取消节点剔除,同时判断后继节点是不是追加到了head节点之后,如果是则就需要唤醒了,因为唤醒线程就是从头结点往后遍历唤醒的。 -
判断当前线程是否满足挂起的条件。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) return true; if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
1> 参数节点node的前置节点pred的waitStatus为SIGNAL,则返回true,证明当前 线程可以被阻塞,返回true。
2> 如果前置节点pred的waitStatus>0则证明被取消,那么由参数节点node从队列中向前遍历,查找到第一个没有被取消的节点,将node追加到找到的节点。
3>如果节点pred的waitstatus不是SIGNAL,则将pred节点状态修改为SIGNAL。2和3均未空,即不满足阻塞条件,所以需要继续抢夺资源并不断更改队列中位于node之前的节点的状态。
该方法是在线程没有获取到锁时自旋转中一直调用的方法,因为一个线程被阻塞必须将其前置节点的状态修改为SIGNAL(因为唤醒时是判断状态为SIGNAL的),所以当前线程的前置节点状态没有修改为SIGNAL则当前线程会一直自旋转尝试将前置节点修改为SIGNAL。当然会存在这种情况:在当前线程一直循环修改前置节点状态时,前置节点获取到了锁,那么前置节点会将状态修改为0,则当前线程会继续循环,要么前置节点执行完毕,要么当前线程将前置节点状态修改为SIGNAL并阻塞当前线程。 -
自我中断。
static void selfInterrupt() { Thread.currentThread().interrupt(); }
该方法调用了Thread的interrupt()方法修改了线程的状态。
-
唤醒线程并判中断状态。
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
-
AQS核心方法。
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) cancelAcquire(node); } }
该方法实现与sychronized的轻量级锁的自旋类似。
1>在循环不断判断是否队列中的第一个节点如果是则抢占资源,抢占成功则将参数节点置为head。
2>如果不满足上述条件则将参数节点的前置节点waitStatue置为SIGNAL,成功则通过调用LockSupprot的park方法阻塞当前线程。
3>如果不成功则一直循环尝试修改参数节点前置节点的waitStatus,指导成功阻塞当前线程。
注意:在修改前置节点的waitStatus时也会一直判断参数节点是否是同步队列中的第一个节点,如果是就会抢占资源的
4>在方法最后,如果线程被中断则调用10 cancelAcquire方法,将参数节点从同步队列中剔除,如果有必要唤醒后续节点。
该方法的逻辑就是AQS的工作流程的实现,包括自旋转、修改前置节点状态、判断线程中断状态以及取消抢占等。 -
抢占资源方法。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
如上述代码所示,先通过tryAcquire()方法抢占资源,抢占不成功则调用addWaiter创建新节点并通过acquireQueued方法入队如果线程被中断过,则从同步队列中剔除并中断自己。
-
释放资源。
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
该方法为释放资源,如果当前线程多次进入锁,即state释放一次后仍不为0,则该方
方法返回false。 -
关于AbstractQueuedSynchronizer中未实现的几个方法:
protected boolean tryAcquire(int arg) protected boolean tryRelease(int arg) protected int tryAcquireShared(int arg) protected boolean tryReleaseShared(int arg)
在AbstractQueuedSynchronizer中已经将所有的基础架构方法实现,那么通过这四个方法从而可以自己扩展实现不同的功能。
以上是介绍了AQS的独占模式的相关方法,下篇介绍其条件等待队列的运行机制及源码并发多线程之AQS源码分析(下)。