本文主要来分析一下AQS共享模式锁的获取和释放,AQS其实只是一个框架,它主要提供了一个int类型的state字段,子类继承时用于存储子类的状态,并且提供了一个等待队列以及维护等待队列的方法。至于如何使用这个状态值和等待队列,就需要子类根据自己的需求来实现了。
以Semaphore类为例,Semaphore允许多个线程同时获得信号量先来看一下Semaphore的接口:
//Semaphore
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
同样的,sync是一个定义在Semaphore中的AQS的抽象子类,在Semaphore类中有两种实现,一个是公平的,一个是非公平的。转到AQS中的acquireSharedInterruptibly
方法,
//AbstractQueuedSynchornizer
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//由于本文分析共享模式锁,所以说tryAcquireShared尝试获取的是permit而不是锁
//tryAcquireShared尝试获取相应数量的permit,如果失败返回负值。返回0代表获取成功但是下次调用会失败,返回正值代表获取成功而且下次调用可能也会成功
//可以理解为返回0代表只有0个permit,所以下次调用会失败,而返回正值代表还有permit,所以下次调用可能会成功
if (tryAcquireShared(arg) < 0)
//获取失败后需要新建一个等待节点并将节点加入等待队列
doAcquireSharedInterruptibly(arg);
}
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//新建一个共享模式的节点并将其加入等待队列
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
//如果其前驱节点是头节点,那么再次尝试获取permit
int r = tryAcquireShared(arg);
if (r >= 0) {
//如果获取成功那么将该节点设置成头节点,并且如果r>0,代表还有剩余的permit,所以如果该节点的后继节点也是共享模式的,就把后继节点也唤醒
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
来看一下setHeadAndPropagate
方法,这个方法和setHead
不同的地方在于它不仅设置了等待队列的头节点,并且检查其后继节点是否可能是共享模式节点,如果是,而且传入的propagate
大于0或者头节点设置了PROPAGATE
状态,那么需要调用doReleaseShared
方法来唤醒后继节点。setHeadAndPropagate
方法的处理过程比较保守,可能会导致很多不必要的唤醒。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
//如果propagate>0,代表有剩余的permit,唤醒共享模式节点
//如果h.waitStatus = PROPAGATE,表示之前的某次调用暗示了permit有剩余,所以需要唤醒共享模式节点
//由于PROPAGATE状态可能转化为SIGNAL状态,所以直接使用h.waitStatus < 0来判断
//如果现在的头节点的waitStatus<0,唤醒
//如果现在的头节点等于null,唤醒
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//如果后继节点为null,whatever唤醒
if (s == null || s.isShared())
doReleaseShared();
}
}
可以看到setHeadAndPropagate
方法的原则是宁滥勿缺,反正doReleaseShared
方法会继续后来的处理:
private void doReleaseShared() {
for (;;) {
Node h = head;
//如果头节点不为空且头节点不等于尾节点,亦即等待队列中有线程在等待
//需要注意的是,等待队列的头节点是已经获得了锁的线程,所以如果等待队列中只有一个节点,那就说明没有线程阻塞在这个等待队列上
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//如果头节点的状态是SIGNAL,代表需要唤醒后面的线程(SIGNAL状态可以看做是后继节点处于被阻塞中)
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//唤醒后继节点
unparkSuccessor(h);
}
//如果头节点的状态为0,说明后继节点还没有被阻塞,不需要立即唤醒
//把头节点的状态设置成PROPAGATE,下次调用setHeadAndPropagate的时候前任头节点的状态就会是PROPAGATE,就会继续调用doReleaseShared方法把唤醒“传播”下去
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//如果头节点被修改了那么继续循环下去
if (h == head) // loop if head changed
break;
}
}
根据自己的思考总结一下,不保证正确性:
- AQS的等待队列的头节点在初始化的时候是个哑节点,其它时候代表已经获取锁的节点(独占模式)或者获取了permit的节点(共享模式),设置了头节点的线程已经可以执行临界区代码了。也就是说,在共享模式下,获得了permit的线程代表的节点可能被其它节点挤出等待队列。总之,等待队列从第二个节点开始才是正在等待的线程。
- AQS的等待队列的节点类Node只有在其后继节点被阻塞的情况下才会是
SIGNAL
状态,所以SIGNAL
状态代表其后继节点正在阻塞中。 - AQS等待队列节点的
PROPAGATE
状态代表唤醒的行为需要传播下去,当头节点的后继节点并未处于阻塞状态时(可能是刚调用addWaiter
方法添加到队列中还未来得及阻塞),就给头节点设置这个标记,表示下次调用setHeadAndPropagate
函数时会把这个唤醒行为传递下去。 - 设置
PROPAGATE
状态的意义主要在于,每次释放permit都会调用doReleaseShared
函数,而该函数每次只唤醒等待队列的第一个等待节点。所以在本次归还的permit足够多的情况下,如果仅仅依靠释放锁之后的一次doReleaseShared
函数调用,可能会导致明明有permit但是有些线程仍然阻塞的情况。所以在每个线程获取到permit之后,会根据剩余的permit来决定是否把唤醒传播下去。但不保证被唤醒的线程一定能获得permit。 - 共享模式下会导致很多次不必要的唤醒。