前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java并发-JUC-AQS-共享模式源码解析

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

作者头像
颍川
发布2021-12-06 15:52:12
2090
发布2021-12-06 15:52:12
举报
文章被收录于专栏:颍川颍川

文章目录

说明

每个 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()方法。

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

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

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

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

代码语言:javascript
复制
     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方法将被调用。从方法名中,您可以看到除了设置新的头节点之外还有一个传播的操作.让我们看看下面的代码:

代码语言:javascript
复制
   //有两个输入参数,一个是成功获取共享锁的节点,另一个是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()方法中也被调用。

代码语言:javascript
复制
    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;
        }
    }

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

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

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

总结

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/04/07 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
    • 说明
      • 疑问
        • 为什么需要实现两种不同模式
        • 什么是共享模式
      • 概述
        • 源码分析
          • 总结
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档