Synchronzier共享模式
.
由于AQS的独占模式release时只能唤醒当前第一个等待的线程,有的时候我们需要通过一次唤醒就能通知多个线程。例如我们常用的CountDownLatch。只要CountDownLatch#countDown方法调用的次数超过了初始化CountDownLatch的次数,那么所有调用CountDownLatch#await的线程将都被唤醒。这是AQS中的独占模式所不能做到的。
同样的我们还是先用一个例子来介绍AQS的共享模式用法。
public class SimpleLatchShared {
private Sync sync = new Sync();
private static class Sync extends AbstractQueuedSynchronizer {
/**
*
*/
private static final long serialVersionUID = 1L;
@Override
protected int tryAcquireShared(int arg) {
// state 1 表示关闭 0表示打开
return getState() == 1 ? 1 : -1;
}
@Override
protected boolean tryReleaseShared(int arg) {
setState(1);
return true;
}
}
public void pass() {
sync.acquireShared(1);
}
public void open() {
sync.releaseShared(1);
}
}
测试代码:
@Test
public void simpleLatchShared() throws InterruptedException {
SimpleLatchShared simpleLatchShared = new SimpleLatchShared();
for (int i = 0; i < 2; i++) {
new Thread(() -> {
simpleLatchShared.pass();
System.out.println(Thread.currentThread().getName() + " Im passed!");
}).start();
}
Thread.sleep(5 * 1000);
System.out.println("open the gate!");
simpleLatchShared.open();
}
测试结果
open the gate!
Thread-0 Im passed!
Thread-1 Im passed!
.
可以看到一次open()的调用,使得2个阻塞的线程被唤醒然后继续执行。假如使用上一节的SimpleLatch的话,那么只会唤醒第一个阻塞的线程。
下面我们来看看AQS的共享模式实现:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
由此可见,当tryAcquireShared返回大于0时,将不会阻塞线程。
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
//可能此线程时被中断唤醒的,并且parkAndCheckInterrupt()中会清除中断标志,所以这里补充一个
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//失败了也就是异常退出时,这里把此次线程的node从同步队列里踢掉
if (failed)
cancelAcquire(node);
}
}
这里的代码实现和独占锁的doAcquire几乎一样,所以这里不做注释了。唯一不同的是当线程满足条件时将会调用setHeadAndPropagate方法,我们再来看此方法的实现。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
//踢掉头结点 设置老二为头结点
setHead(node);
//tryAcquireShared返回大于0 唤醒此刻的老二
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
propagate 也就是tryAcquireShared的返回值,如果大于0则doReleaseShared,此方法会唤醒此节点的后继节点,所以单tryAcquireShared返回为0时,不会唤醒多个线程。
private void doReleaseShared() {
//
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//如果ws == Node.SIGNAL 那就说明后老二已经设置了老大为SIGNAL ,
//老二可能在休眠了,所以自旋设置为0然后尝试唤醒老二,让老二去尝试获取资源。
//(其实我也不懂为啥要自旋设置状态为0)
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
//状态是0,说明老二还没有来得及设置head的状态,老二还活着,这里自旋设置自己为PROPAGATE,
//那么下次老二自旋则会设置头为SINGAL 然后再次尝试获取资源。
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//为啥要判断头结点是否发生变化呀?
//有可能其他线程修改了其他节点为头结点,使得我们通知了现在的头结点的后继节点
//而不是进入方法时的头结点,为了避免丢失此次通知,在尝试一遍。
if (h == head) // loop if head changed
break;
}
}
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);
}
好了 AQS的难度还是非常大的,真的是把我看醉了,很怀疑自己的智商。希望以后自己也能写出这么吊的代码吧。
后面几张我将会看看那些基于AQS实现的同步工具,例如ReentrantLock,CountDownLatch之类的吧。