在《JUC之AbstractQueuedSynchronizer基本介绍》中,我们介绍了AbstractQueuedSynchronizer在独占模式下的使用,接下来看看其在共享模式下的使用。
共享模式
(1)acquireShared
acquire方法提供了在独占模式下的竞争资源(资源即锁)策略,acquireShared方法则提供了在共享模式下的竞争资源策略。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
(2)tryAcquireShared
tryAcquireShared竞争资源的方法,由子类重写该方法去实现自定义具体的竞争逻辑。方法返回剩余共享资源的数量,大于或等于0表示竞争资源成功,获取到锁;小于0表示竞争资源失败,没有获取到锁。(假设当剩余共享资源的数量为3的时候,一个线程执行完tryAcquireShared方法之后,资源数量会减1返回值为2;当共享资源数量为1的时候,线程获取资源成功,返回值为0;此时下一个线程继续获取资源,已经没有可用资源了,返回值返回-1,锁获取失败)
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
(3)doAcquireShared
如果经过tryAcquireShared没有获取到锁,则进入入队和线程休眠的逻辑。该方法和在独占模式下的方法acquireQueued实现的思想大致相同,即将竞争锁失败的线程节点添加到等待队列中,并开始自旋。如果该节点是第一个线程节点则立即尝试去获取锁。获取锁失败的情况下,继续判断是否满足线程休眠的逻辑,如果满足,则将当前线程休眠,直到满足条件被唤醒。共享模式与独占模式下的不同之处在于获取锁的过程和当节点的线程获取到锁时,对该线程节点的处理。
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);
// r>=0表示获取锁成功
if (r >= 0) {// 获取锁成功,接下来执行释放该节点的逻辑
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 没有获取到锁的线程,查看是否要将其休眠。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
setHeadAndPropagate:
当前线程节点获取到锁之后的后续处理工作。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// 仍然是将当前节点替换成空的头结点。
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
对于条件语句:if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0),可将其分为如下几个逻辑部分,当一个满足条件的时候就可以进入方法体。依次判断下面的条件是否满足,只要满足就会立即返回true,后面的判断就不会进行下去了。
propagate > 0
h == null
h.waitStatus < 0
(h = head) == null
h.waitStatus < 0
①propagate > 0
propagate的值为tryAcquireShared方法的返回值,表示了剩余可用共享资源的数量,其大于0表示仍然有可用资源。
②h == null
Node h = head; h引用指向头节点,h == null即head == null,什么情况下head是为null的呢?当队列还没有初始化的时候。
③h.waitStatus < 0
waitStatus等待状态不为默认值和CANCELLED==1的状态
④(h = head) == null
将h的引用指向新的head,即node对象。
⑤h.waitStatus < 0
依然判断waitStatus等待状态不为默认值和CANCELLED==1的状态
关于②与④步骤中的判断操作,是避免在多线程并发的操作中,在该线程还在处理这里逻辑的时候,其他获取到锁的队列线程断开了头节点的连接,然后GC回收了头结点。判空操作为了代码的安全性,避免在某些情况下出现空指针的问题。
doReleaseShared:
唤醒等待队列中的等待线程操作。
释放队列中所有的符合竞争条件的等待线程节点让他们再去竞争资源。不同于独占模式下的释放队列的操作,在独占模式下一次只释放第一个符合竞争条件的线程节点。共享模式下是释放所有。
// 头指针的下一个节点存在,且是共享模式的
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
rivate void doReleaseShared() {
for (;;) {
Node h = head;
// h!=null排除队列还未初始化的情况
// h != tail排除队列虽然已经初始化了,但是还没有线程节点
// 这两个条件是保证队列中有线程节点。
if (h != null && h != tail) {
int ws = h.waitStatus;
// waitStatus为SIGNAL=-1表示在等待队列中已经休眠的线程
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 唤醒h节点对应的线程
unparkSuccessor(h);
}
// waitStatus==0默认值,需要变更为PROPAGATE=-3
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}