上一篇文章讲到,ReentrantLock方法的实现全部是依靠Sync的方法。而Sync又是继承了AQS,所以需要重点分析AQS。
AQS的设计是采用模板方法模式的。即如果要使用AQS,就需要继承AQS并重写AQS里指定的方法,以下方法可以按照需要被重写:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
tryAcquire(int arg)
, 独占式获取同步状态,实现该方法需要查询当前状态并判定同步状态是否符合预期,然后再进行CAS设置同步状态。
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
tryRelease(int arg)
, 独占式释放同步状态。
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
tryAcquireShared(int arg)
, 共享式获取同步状态,返回大于等于0的值,表示获取成功,反之获取失败。
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
tryReleaseShared(int arg)
, 共享式释放同步状态。
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
isHeldExclusively()
, 当前AQS是否在独占模式下被线程占用,一般该方法表示是否被当前线程独占。
而重写这些方法的时候需要操作 arg,即访问或者修改状态,AQS提供了以下方法来操作 arg:
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
//使用CAS设置当前状态,保证操作的原子性
protected final boolean compareAndSetState
(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset,
expect, update);
}
当前线程获取同步状态失败时,进程会被阻塞。AQS是怎么维持这些被阻塞线程的信息的呢?答案是,同步队列,这是一个FIFO双向队列。AQS会将当前线程以及等待状态等信息构造成一个Node节点并加入到同步队列,同时会阻塞线程。当同步状态(锁)被释放时,会将首节点的线程唤醒,使其可以再次尝试获取同步状态。
节点是构成同步队列的基础,AQS拥有首节点和尾节点,没有成功获取同步状态的线程将会被构造成一个节点加入到队列的尾部。
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
看一下Node的定义,它是AQS的静态内部类:
static final class Node {
/**一个标记,指明节点在共享模式下等待同步状态 */
static final Node SHARED = new Node();
/** 一个标记,指明节点在独占模式下等待同步状态*/
static final Node EXCLUSIVE = null;
//以上两个值是 nextWaiter 域的取值
/** 在同步队列中等待的线程由于超时或被中断,将标记为此状态,意思是取消等待同步状态 */
static final int CANCELLED = 1;
/**表明当前节点如果释放了状态或者被取消,则通知后继节点,使得后继节点的线程得以运行 */
static final int SIGNAL = -1;
/** 表明节点在等待一个Condition的signal,
*该节点处于等待队列中, 当其他线程调用signal()方法时,
*该节点将会从等待队列中转移到同步队列,加入到对同步状态的获取中
*/
static final int CONDITION = -2;
/**
*表示下一次共享式同步状态的获取将会无条件的传播下去。
*表明一个共享锁的释放将会传播到其他节点,而不是仅仅后继节点。
*这个状态仅仅在 doReleaseShared()中对头节点进行设置。
*/
static final int PROPAGATE = -3;
/**
* The values are arranged numerically to simplify use.
* Non-negative values mean that a node doesn't need to
* signal. So, most code doesn't need to check for particular
* values, just for sign.
*
* The field is initialized to 0 for normal sync nodes, and
* CONDITION for condition nodes. It is modified using CAS
* (or when possible, unconditional volatile writes).
*/
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
*获取当前节点的前驱节点,没有则抛出异常
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {}
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的定义,下面就来分析一下独占式同步状态获取与释放。
通过调用 AQS 的 void acquire(int arg)
方法可以获取同步状态:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
其中tryAcquire方法是需要重写的方法。
分析:
流程: 首通过自定义的 tryAcquire(arg)
方法获取同步状态,成功的话直接返回,失败的话就调用 addWaiter(Node.EXCLUSIVE), arg)
将当前线程构造为一个独占模式的 Node,加入到同步队列尾部。节点进入同步队列之后,就进入了一个自旋过程,即调用 acquireQueued()
使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断 selfInterrupt()
,将中断补上。
构造节点并将其加入到同步队列的代码很简单,就不多说了:
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;
}
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;
}
}
}
}
重点看一下 acquireQueued()
的代码:
final boolean acquireQueued(final Node node, int arg) {
//标记是否成功获取同步状态,成功为false。
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;
}
//前一节点不是头节点或者获取状态失败,就检查当前线程是否可以进入
//waiting状态,如果可以,就park当前线程,进入waiting状态,并检查中断。
//在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
来看看这一小段代码,很有意思:
...
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
...
当前面的获取状态失败时,就检查线程是否可以去休息(进入waiting状态,等待其他线程的唤醒),shouldParkAfterFailedAcquire(p, node)
函数名很好地说明了这一点,如果可以去休息,就去 parkAndCheckInterrupt()
,即阻塞线程,并检查是否被中断过,如果被中断过,就设置 interrupted = true
。具体细节如下:
private static boolean shouldParkAfterFailedAcquire
(Node pred, Node node) {
int ws = pred.waitStatus;//拿到前驱的状态
if (ws == Node.SIGNAL)
//如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
return true;
if (ws > 0) {
/*
* 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
* 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被GC回收!
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果前驱正常,那就把前驱的状态设置成SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//调用park()使线程进入waiting状态
return Thread.interrupted();//如果被唤醒(注意此时线程已经被唤醒),查看自己是不是被中断的。
}
线程被唤醒的时候,就去检查中断标志位有没有置位,如果置位则返回true,继而 boolean acquireQueued(final Node node, int arg)
返回true,继而在 final void acquire(int arg)
中就执行 selfInterrupt()
,进行自我中断。这也就是 final void acquire(int arg)
方法对中断不敏感的原因。
接着是独占模式的释放:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head; //找到头节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); //唤醒等待队列里的下一个线程
return true;
}
return false;
}
比较简单,重点是 unparkSuccessor(h)
:
private void unparkSuccessor(Node node) {
//这里,node一般为当前线程所在的结点。
int ws = node.waitStatus;
if (ws < 0)//置零当前线程所在的结点状态,允许失败。
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;//找到下一个需要唤醒的结点s
if (s == null || s.waitStatus > 0) {//如果为空或已取消
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
//从这里可以看出,<=0的结点,都是还有效的结点。
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒
}
此外,还有一个响应中断的获取同步状态的函数:
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException(); //直接抛出异常
}
} finally {
if (failed)
cancelAcquire(node);
}
}
它和不响应中断版本的函数很像,只是在这里,如果线程被中断过,直接抛出异常。