Java并发-JUC-AQS-共享模式源码解析

文章目录

        • 说明
        • 疑问
          • 为什么需要实现两种不同模式
          • 什么是共享模式
        • 概述
        • 源码分析
        • 总结

说明

每个 Java 工程师都应该或多或少地了解 AQS,我已经反复研究了很长时间,忘记了一遍又一遍地看它.每次我都有不同的经历.这一次,我打算重新拿出系统的源代码,并将其总结成一系列文章,以供将来查看.

一般来说,AQS规范是很难理解的,本次准备分五篇文章用来分析AQS框架:

  1. 第一篇(翻译AQS论文,理解AQS实现思路)
  2. 第二篇(介绍AQS基础属性,内部类,抽象方法)
  3. 第三篇(介绍独占模式的代码实现)
  4. 第四篇(介绍共享模式的代码实现)
  5. 第五篇(介绍Condition的相关代码实现)

疑问

为什么需要实现两种不同模式

大师给的解释是,虽然大多数应用程序应最大程度地提高总吞吐量,最大程度地容忍缺乏饥饿的概率。但是,在诸如资源控制之类的应用程序中,保持跨线程访问的公平性,容忍较差的聚合吞吐量更为重要,没有任何框架能够代表用户在这些相互冲突的目标之间做出决定;相反,必须适应不同的公平政策。所以AQS框架提供了两种模式

什么是共享模式

共享模式:允许多个线程同时持有资源;

概述

本篇文章为系列文章的第四篇,本篇文章介绍AQS共享模式的代码实现,首先,我们从总体过程入手,了解AQS的执行逻辑,然后逐步深入分析了源代码。

获取锁的过程:

  1. acquireShared()申请资源,如果能够申请成功,它将进入临界区,申请成功的标示是用户自己实现的方法tryAcquireShared大于0,
  2. tryAcquireShared小于0,它进入一个 FIFO 等待队列并被阻塞,等待唤醒
  3. 当队列中的等待线程被唤醒时,会再次尝试获取锁资源。如果成功,它进入临界区,否则它将继续阻塞,等待唤醒
    释放锁过程:
  4. 当线程调用releaseShared()来释放锁资源时,如果没有其他线程在等待锁资源,那么释放就完成了。
  5. 如果队列中有其他正在等待锁资源的线程需要被唤醒,则队列中的第一个等待节点(FIFO)将被唤醒。

源码分析

基于上面提到的获取和释放排他锁的一般过程,让我们来看看源代码实现逻辑.首先,让我们看看获取锁的acquireShared()方法。

  public final void acquireShared(int arg) {
      //试图获取共享锁。返回值小于0表示获取失败
        if (tryAcquireShared(arg) < 0)
            //获取锁失败后执行方法
            doAcquireShared(arg);
  }

这里,tryacquisharered()方法留给用户来实现特定获取锁的逻辑.关于这个方法的实现有两点

  1. 该方法必须检查当前上下文是否支持获取共享锁,以及是否支持再次获取共享锁。
  2. 此方法的返回值是一个重要的点。首先,从上面的源代码片段可以看出,如果返回值小于0,则表示锁获取失败,需要进入等待队列。
    第二,如果返回值等于0,则表示当前线程成功获取共享锁,但其后续线程无法继续获取共享锁,即不需要唤醒在其后面等待的节点。
    最后,如果返回值大于0,则表示当前线程成功获取共享锁,其等待的节点可以继续成功获取共享锁,即需要唤醒后续节点尝试获取共享锁。

根据上面的分析,让我们来看看doAcquireShared方法的实现

     private void doAcquireShared(int arg) {
         //添加等待节点(与独占锁的唯一区别是节点类型变为共享类型)
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取节点的前节点
                final Node p = node.predecessor();
                //p == head 表示上一个节点已经获取了锁,当前节点将尝试获取它
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    //注意,等于0表示不需要唤醒后续节点,大于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方法将被调用。从方法名中,您可以看到除了设置新的头节点之外还有一个传播的操作.让我们看看下面的代码:

   //有两个输入参数,一个是成功获取共享锁的节点,另一个是tryacquisharered方法的返回值。注意,它可能大于或等于0
   private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; //   记录当前的头节点
        //设置一个新的头节点,即将获得锁的节点设置为头节点
        //注意:这里是获取锁后的操作,不需要并发控制
        setHead(node);
        //有两种情况需要执行唤醒操作
        //1.Propagate>0 表示调用方指示需要唤醒后续节点
        //2.头节点后面的节点需要被唤醒(waitstatus < 0),无论它是旧的头节点还是新的头节点
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //如果当前节点的后继节点是共享类型的或者没有后继节点,它将被唤醒
            //可以理解,除非明确表示不需要唤醒(后续等待节点是独占的),否则都需要唤醒
            //s.isShared() 在第二篇中有介绍
            if (s == null || s.isShared())
                //我稍后再详细说
                doReleaseShared();
        }
    }
    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

最后的唤醒操作也很复杂,所以我特地把它拿出来分析.
注意:唤醒操作在releasshare()方法中也被调用。

    private void doReleaseShared() {
        for (;;) {
            //唤醒操作从头节点开始.注意,这里的头节点已经是上面新设置的头节点
            //实际上,它是唤醒新获得共享锁节点的后继节点
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //后继节点需要唤醒
                if (ws == Node.SIGNAL) {
                    //这里需要并发控制,因为这里有setHeadAndPropagate和Release两个操作,避免了两次unpark(接触阻塞)
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    //执行唤醒操作
                    unparkSuccessor(h);
                }
                //如果后续节点不需要临时唤醒,则当前节点状态被设置为PROPAGATE,以确保它在将来可以被传播
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            //如果头部节点没有变化,则表示设置完成,循环退出
            //如果head节点改变了,例如,其他线程得到了锁,为了使唤醒动作可以被传递,他必须再次尝试
            if (h == head)                   // loop if head changed
                break;
        }
    }

接下来,让我们看看释放共享锁的过程

   public final boolean releaseShared(int arg) {
        //试图释放共享锁
        if (tryReleaseShared(arg)) {
            //唤醒过程,详见上述分析
            doReleaseShared();
            return true;
        }
        return false;
    }

注意:上面的setHeadAndPropagate()方法表明等待队列中的线程成功地获得了共享锁。此时,它需要唤醒它后面的共享节点(如果有的话)。但是,当共享锁通过releasshared()方法释放时,可以唤醒等待排他锁和共享锁的线程来尝试获取它。

总结

与排他锁相比,共享锁的主要特点是当等待队列中的共享节点成功获得锁(即获得共享锁)时,由于它是共享的,所以必须依次唤醒所有可以与其共享当前锁资源的节点.毫无疑问,这些节点也一定在等待共享锁(这是前提,如果您在等待共享锁),我们可以以读写锁为例.当读锁定被释放时,读锁定和写锁定都可以争用资源。

你可能感兴趣的:(并发,java,juc,AQS,并发)