目录
2.2 setHeadAndPropagate(node, r)
1 前言
本人使用jdk8版本。
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以文件的读写为例,如果一个程序对该文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问,两种不同的访问模式在同一时刻对文件或者资源的访问情况。
左半部分共享式访问资源时,其他共享式的访问均被允许,而独占式访问被阻塞,右半部分是独占式访问资源时,同一时刻其他访问均被阻塞。共享锁的获取与独占锁的获取与释放有很多相似之处,下面介绍会简单一下,补充知识可以参考:AbstractQueuedSynchronizer独占式同步状态获取与释放。
2 acquireShared方法
若tryAcquireShared()返回false,则调用doAcquireShared()。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
2.1 doAcquireShared
首先会将当前线程构造成一个共享节点,然后在一个无限循环中获取锁,两次尝试失败后会会陷入阻塞(LockSupport.park()),直到被中断或被前驱节点唤醒,然后会继续获取锁。若获取锁成功,则调用setHeadAndPropagate()将当前节点设为头结点,并将后面阻塞的共享节点唤醒,唤醒的节点又会获取读锁,就这样使读锁的获取不断传播下去。
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); // 成功返回1
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);
}
}
2.2 setHeadAndPropagate(node, r)
这里的r即下面的propagate参数是1,一般情况下在设置头结点后直接执行if体中的代码。若node的后继节点不为空且是共享节点,那么就调用doReleaseShared(),唤醒node的阻塞的后继节点。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
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();
}
}
3 releaseShared方法
尝试释放锁失败就执行doReleaseShared()方法。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
3.1 doReleaseShared方法
一般情况下,for循环只执行一次就会退出。若当前节点的waitStatus是SIGNAL,则表示后继节点处于阻塞状态,直接唤醒。这里唤醒的可能是共享节点也可能是独占节点。若是独占节点,又会尝试获取锁,检测到读锁被持有,又会陷入阻塞;若是共享节点,直接获取锁,调用setHeadAndPropagate方法进行共享锁获取的传播。
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) { // 后继节点处于阻塞状态
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h); // 唤醒后继节点
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // 头结点没有改变就退出循环
break;
}
}
4 cancelAcquire方法
在acquireQueued()中,若执行中抛出异常,会执行finally块,取消当前节点对锁的获取。
首先从node往前找到一个没有取消的节点,令node.prev等于它,相当于把中间取消的节点全部移出队列。然后将node的waitStatus设置为CANCELLED。在判断node是否在队列尾部,在就把node出队。否则判断node的前驱是不是head,是的话调用unparkSuccessor(node)唤醒node的后继节点,那么node永远也没有机会退出阻塞了;不是的话令pred.next = node.next,相当于把node出队。
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// 从node往前,将所有waitStatus为CANCELLED的节点移出队列
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
// 将当前节点状态设为CANCELLED
node.waitStatus = Node.CANCELLED;
// 若当前节点为尾结点,则移出队列
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// 若node不是第二个节点,则直接把node移出队列
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的后继节点
}
node.next = node; // help GC
}
}