【Java并发编程】AQS(4)——共享锁的获取与释放

今天来说下共享锁的获取与释放,建议大家在看这篇文章之前,先将我写的关于独占锁的文章看一下,其中涉及了许多重复的方法,在这篇文章中就不会再次讲解了。好了,我们先来看共享锁的获取吧

一.  共享锁的获取

在AQS中共享锁的获取一共有三个方法,今天主要讲第一个

  1. acquireShared:不响应中断获取锁

  2. acquireSharedInterruptibly:响应中断获取锁

  3. tryAcquireSharedNanos:响应中断与超时获取锁

在读源码前,我们需要注意两点:

  1. 共享锁模式与独占锁模式最大的区别在于独占锁模式同一时间只能有一个线程持有锁,临界区只能被串行访问;共享锁模式在同一时间可以有多个线程持有锁

  2. 在共享锁模式下,获取锁和释放锁成功后,都会去唤醒后继Node;独占锁模式下只在释放锁成功后才唤醒后继Node

开始看acquireShared源码吧 

public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

 

1  tryAcquireShared

这个方法是由子类实现的,其功能是用来获取共享锁,具体的实现会在后续的文章中说道,但这里我们需要提前了解下返回值的含义(下面源码只将返回值的注释保留了下来)

/**
* @return a negative value on failure; zero if acquisition in shared
*         mode succeeded but no subsequent shared-mode acquire can
*         succeed; and a positive value if acquisition in shared
*         mode succeeded and subsequent shared-mode acquires might
*         also succeed, in which case a subsequent waiting thread
*         must check availability. (Support for three different
*         return values enables this method to be used in contexts
*         where acquires only sometimes act exclusively.)  Upon
*         success, this object has been acquired.
*/
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

我们可以看到返回值是int值,我们通过注释可以知道这个int型返回值的含义

  1. 返回值<0:代表失败

  2. 返回值=0:代表获取到锁但后继节点获取锁是不能成功的

  3. 返回值>0:代表获取锁成功且后继节点获取锁可能会成功

了解返回值的意义后,我们再回到acquireShared方法,如果tryAcquireShared返回值小于0,说明获取共享锁失败了,因此我们就要执行doAcquireShared

2  doAcquireShared

在看这个方法之前,再去看一眼"独占锁获取"中说的acquireQueued方法。。。看完了吧,我们现在来看下doAcquireShared方法 

是不是和 acquireQueued很像,其实他们逻辑都是一样的,且里面大部分方法都相同,其中的不同点我用红框标注出来了

我们先看第一个不同点,这个就是"独占锁获取"中用到的入队方法,只不过共享锁模式中把入队和唤醒操作都放doAcquireShared一个方法里,这里就不再赘述了

我们再看第二个。如果此Node的前驱Node是head,则会再次通过tryAcquireShared去获取共享锁,如果返回状态是大于等于0,说明获取共享锁成功,就会进入到if分支内,否则就会跳到外面的第二个if分支去执行挂起操作。我们具体看获取锁成功后调用的setHeadAndPropagate方法

2.1  setHeadAndPropagate

这个方法主要完成两件事,设置头节点head以及在一定条件下唤醒后继Node。还记得这节最开始说过,共享锁模式不仅会在释放锁成功后唤醒后继Node,在获取锁成功时也会唤醒后继Node,所以setHeadAndPropagate方法会在两个不同的地方被调用,我们具体看代码


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();
    }
}

首先我们通过setHead方法,重新设置头结点,即将当前获取锁的thread置空,然后将head指向当前node,这里需要特别注意:不管是独占锁获取成功还是共享锁获取成功,都会通过setHead方法将head指向自己,换一句话说,head指向的Node就是最新获取到锁的Node(只有在获取锁成功后才调用setHead更改head)

然后我们会在一定条件下来决定是否唤醒继Node,最终唤醒后继Node的是通过doReleaseShared方法,因为锁的释放也要调用此方法,所以我们把这个方法放在下一小节一起讨论

二.  共享锁的释放

AQS中对共享锁的释放方法只有一个,就是releaseShared方法,我们直接看源码


public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

可以看到,就两个方法,其中第一个方法tryReleaseShared的方法是子类复写的方法,返回值为boolean,如果失败,则直接跳到最后返回false;否则就返回true,然后进入if分支调用doReleaseShared方法来唤醒后继Node。没错这个doReleaseShared方法就是上一节最后没讲的那个方法,也是很重要的一个方法,我们具体来看看吧

1  doReleaseShared


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)                   // loop if head changed
            break;
    }
}

我们可以看到,doReleaseShared方法里面又是个自旋for循环,在分析自旋for循环里面的代码之前,我们先来想想这里为什要自旋?退出自旋的条件是什么?

我们是共享锁模式,所以允许多线程同时拥有锁,当某个线程正在执行doReleaseShared方法的时候,有个新的线程拿到了锁,并执行了setHead方法,此时头结点head就发生了变化(这个变化如下图所示),所以我们就需要重新拿到新的头结点head的状态来再去唤醒后面的Node

因此,我们退出的自旋的条件就是在执行doReleaseShared时head是没有更改的,如果更改,说明当时拿到的head的后继Node此时已经被唤醒了,所以我们需要通过自旋来重新定位到此时新的head并再次唤醒后继Node

这里大家可能会很好奇,什么时候会出现上面说的head更换的情况?这种情况的发生一共有两种,我们结合上面的图来说明,假设图中Node1和Node2中的线程分别为t1和t2

  1. t1执行完Node h = head但还未执行if (h == head)时,有持有锁的线程成功释放了锁,并唤醒了Node2中的线程t2,t2拿锁成功后执行了setHead

  2. t1执行完upparkSuccessor后但还未执行if (h == head)时,唤醒的t2成功拿到了锁并执行了setHead

当然,当t1执行到if (h == head),如果if条件成立,即h == head,说明在t1执行doReleaseShared方法期间没有其他线程执行了setHead方法,所以t1可以退出自旋for循环

弄清了上面两个问题后,后面就轻松多了,我们具体看下doReleaseShared方法的代码吧

进入自旋for循环后,我们会拿到此时的头结点,然后我们会判断,只要同步队列已初始化且有等待的Node,则会进入最外层的if分支,里面有两个分支,对应着两种情况,我们先来看第一个if分支


if (ws == Node.SIGNAL) {
    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
        continue;            // loop to recheck cases
    unparkSuccessor(h);
}

如果h的状态是SIGNAL,说明其后继Node是等待被唤醒的,然后我们会先将h的状态置为初始值0,因为这里可能会存在多个线程并发调用,所以这步会通过CAS操作保证只有一个线程能成功,失败的就自旋重来。成功后会调用unparkSuccessor唤醒后继 Node。需要注意,这里的并发情况只可能是下面两种

  1. 多个释放锁成功后调用doReleaseShared的线程加一个获取锁成功后调用doReleaseShared的线程

  2. 多个释放锁成功后调用doReleaseShared的线程

获取锁的线程为什么只有一个?因为前面说了,每个线程获取锁成功后会重置head,不明白的再回到前面看下

我们接着看else if 分支

else if (ws == 0 &&
         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
    continue;

什么时候ws==0?一种是上面if分支中的compareAndSetWaitStatus(h, Node.SIGNAL, 0)执行成功后,但显然这里不是这种情况,因为如果执行了上面的if分支,就不会进入到else if里面了

还有一种情况是在head初始化时(即同步队列初始化)。那什么时候head会初始化呢?在独占锁获取中我们讲过,只有当有Node入队时,才会初始化head。假设此时有个Node要入队,发现head为null后,就会通过enq方法初始化head,然后再将head置为SIGNAL。所以从head初始化到被置为SIGNAL这很短的一段时间内,head的状态是为0的。所以此时我们会将head的状态置为propagation,告诉其他线程后继节点可能是需要被唤醒的

好了,共享锁的获取与释放就讲到这里了,本来今天还想早点睡的,结果写完就到这个时候了,AQS系列还有最后一篇,应该会在这个周末更新完,困死了,晚安

(未完)

欢迎大家关注我的公众号 “程序员进阶之路”,里面记录了一个非科班程序员的成长之路

                                                         

发布了117 篇原创文章 · 获赞 260 · 访问量 21万+

猜你喜欢

转载自blog.csdn.net/qq_36582604/article/details/105424796
今日推荐