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

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

作者头像
颍川
发布于 2021-12-06 07:52:12
发布于 2021-12-06 07:52:12
24000
代码可运行
举报
文章被收录于专栏:颍川颍川
运行总次数:0
代码可运行

文章目录

说明

每个 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
代码运行次数:0
运行
AI代码解释
复制
  public final void acquireShared(int arg) {
      //试图获取共享锁。返回值小于0表示获取失败
        if (tryAcquireShared(arg) < 0)
            //获取锁失败后执行方法
            doAcquireShared(arg);
  }

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
     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
代码运行次数:0
运行
AI代码解释
复制
   //有两个输入参数,一个是成功获取共享锁的节点,另一个是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
代码运行次数:0
运行
AI代码解释
复制
    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
代码运行次数:0
运行
AI代码解释
复制
   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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
(juc系列)aqs源码学习笔记
java.util.concurrent.locks.AbstractQueuedSynchronizer.
呼延十
2021/10/08
3290
Java并发之AQS源码分析(二)
我在 Java并发之AQS源码分析(一)这篇文章中,从源码的角度深度剖析了 AQS 独占锁模式下的获取锁与释放锁的逻辑,如果你把这部分搞明白了,再看共享锁的实现原理,思路就会清晰很多。下面我们继续从源码中窥探共享锁的实现原理。
张乘辉
2019/06/14
4010
Java并发之AQS源码分析(二)
深入浅出AQS之共享锁模式
原文链接:https://www.jianshu.com/p/1161d33fc1d0
天涯泪小武
2019/09/27
8540
Java并发:深入浅出AQS之共享锁模式源码分析
1、当线程调用 acquireShared()申请获取锁资源时,如果成功,则进入临界区。 2、当获取锁失败时,则创建一个共享类型的节点并进入一个FIFO等待队列,然后被挂起等待唤醒。 3、当队列中的等待线程被唤醒以后就重新尝试获取锁资源,如果成功则唤醒后面还在等待的共享节点并把该唤醒事件传递下去,即会依次唤醒在该节点后面的所有共享节点,然后进入临界区,否则继续挂起等待。
搜云库技术团队
2019/10/18
8910
AQS源码解析(2)——共享模式
在Java并发包下,Semaphore(信号量)工具类就是使用AQS共享模式的一种实现。Semaphore的使用方式如下。
黑洞代码
2021/01/14
3820
AQS源码解析(2)——共享模式
AbstractQueuedSynchronizer 原理分析 - 独占/共享模式
AbstractQueuedSynchronizer (抽象队列同步器,以下简称 AQS)出现在 JDK 1.5 中,由大师 Doug Lea 所创作。AQS 是很多同步器的基础框架,比如 ReentrantLock、CountDownLatch 和 Semaphore 等都是基于 AQS 实现的。除此之外,我们还可以基于 AQS,定制出我们所需要的同步器。
田小波
2018/05/02
3.7K11
AbstractQueuedSynchronizer 原理分析 - 独占/共享模式
JAVA面试备战(十六)--AQS共享锁的获取与释放
在前面两篇系列文章中,已经讲解了独占锁的获取和释放过程,而共享锁的获取与释放过程也很类似,如果你前面独占锁的内容都看懂了,那么共享锁你也就触类旁通了。
程序员爱酸奶
2022/04/12
4650
精美图文讲解Java AQS 共享式获取同步状态以及Semaphore的应用
你有一个思想,我有一个思想,我们交换后,一个人就有两个思想 If you can NOT explain it simply, you do NOT understand it well enough
用户4172423
2020/06/19
3750
精美图文讲解Java AQS 共享式获取同步状态以及Semaphore的应用
读写锁原理解读
读写锁是一对互斥锁,分为读锁和写锁。读锁和写锁互斥,让一个线程在进行读操作时,不允许其他线程的写操作,但是不影响其他线程的读操作;当一个线程在进行写操作时,不允许任何线程进行读操作或者写操作。读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个
一个风轻云淡
2023/10/15
4380
读写锁原理解读
Java并发编程之AQS以及源码解析
AQS(AbstractQueuedSynchronizer)是 Doug Lea 大师创作的用来构建锁或者其他同步组件(信号量、事件等)的基础框架类。
Java技术债务
2022/09/26
6870
Java并发编程之AQS以及源码解析
别走!这里有个笔记:图文讲解 AQS ,一起看看 AQS 的源码……(图文较长)
" AbstractQueuedSynchronizer 抽象队列同步器,简称 AQS 。是在 JUC 包下面一个非常重要的基础组件,JUC 包下面的并发锁 ReentrantLock 、 CountDownLatch 等都是基于 AQS 实现的。所以想进一步研究锁的底层原理,非常有必要先了解 AQS 的原理 "
程序员小航
2020/11/23
5210
别走!这里有个笔记:图文讲解 AQS ,一起看看 AQS 的源码……(图文较长)
AQS源码剖析第三篇--共享模式
本文先用 CountDownLatch 将共享模式说清楚,然后顺着把其他 AQS 相关的类 CyclicBarrier、Semaphore 的源码一起过一下。
大忽悠爱学习
2022/10/04
3200
AQS源码剖析第三篇--共享模式
Java 并发(3)AbstractQueuedSynchronizer 源码分析之共享模式
通过上一篇《Java 并发(2)AbstractQueuedSynchronizer 源码分析之独占模式》的分析,我们知道了独占模式获取锁有三种方式,分别是不响应线程中断获取,响应线程中断获取,设置超时时间获取。在共享模式下获取锁的方式也是这三种,而且基本上都是大同小异,我们搞清楚了一种就能很快的理解其他的方式。
JavaFish
2020/03/21
5690
Semaphore 源码分析
Semaphore 源码分析 1. 在阅读源码时做了大量的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限,并且代码阅读起来没有 IDE 方便,所以在 github 上提供JDK1.8 的源码、详细的注释及测试用例。欢迎大家 star、fork ! 2. 由于个人水平有限,对源码的分析理解可能存在偏差或不透彻的地方还请大家在评论区指出,谢谢! 1. Semephore 简单介绍    一般来说,在 Java 中比较常用的同步工具就是 Lock 和 Synchronized 但是 Java 也
lwen
2018/04/17
7460
Semaphore 源码分析
扔掉源码,15张图带你彻底理解java AQS
java中AQS是AbstractQueuedSynchronizer类,AQS依赖FIFO队列来提供一个框架,这个框架用于实现锁以及锁相关的同步器,比如信号量、事件等。
jinjunzhu
2022/08/23
7400
扔掉源码,15张图带你彻底理解java AQS
Java并发之AQS详解[通俗易懂]
今天学了学并发AQS机制,是抽象队列同步器,用户主要通过继承AQS类,来实现自定义锁,从而完成特定功能,AQS提供了两种锁(1)共享锁(2)排他锁。 下面这个博客介绍的AQS机制挺不错可以看看 原文链接 一、概述   谈到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)!
全栈程序员站长
2022/09/22
6670
JUC学习之共享模型工具之JUC并发工具包上
全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架
大忽悠爱学习
2022/05/06
4340
JUC学习之共享模型工具之JUC并发工具包上
AQS源码分析[通俗易懂]
当我们提到 juc 包下的锁,就不得不联系到 AbstractQueuedSynchronizer 这个类,这个类就是大名鼎鼎的 AQS,AQS 按字面意思翻译为抽象队列同步器,调用者可以通过继承该类快速的实现同步多线程下的同步容器。不管是我们熟悉的 ReadWriteLock 亦或是 ReentrantLock,或者 CountDownLatch 与 Semaphore,甚至是线程池类 ThreadPoolExecutor 都继承了 AQS。
全栈程序员站长
2022/09/23
5980
AQS (Abstract Queued Synchronizer)源码解析 -- 独占锁与共享锁的加锁与解锁
AQS (Abstract Queued Synchronizer) 是 JDK 提供的一套基于 FIFO 同步队列的阻塞锁和相关同步器的一个同步框架,通过 AQS 我们可以很容易地实现我们自己需要的独占锁或共享锁。 java 中,我们曾经介绍过的信号量、ReentrantLock、CountDownLatch 等工具都是通过 AQS 来实现的。
用户3147702
2022/06/27
8780
AQS (Abstract Queued Synchronizer)源码解析 -- 独占锁与共享锁的加锁与解锁
AQS共享模式与并发工具类的实现
一行一行源码分析清楚 AbstractQueuedSynchronizer (三)
Java技术江湖
2019/09/25
3470
相关推荐
(juc系列)aqs源码学习笔记
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验