精美图文讲解Java AQS 共享式获取同步状态以及Semaphore的应用

简介: 精美图文讲解Java AQS 共享式获取同步状态以及Semaphore的应用

| 好看请赞,养成习惯


  • 你有一个思想,我有一个思想,我们交换后,一个人就有两个思想
  • If you can NOT explain it simply, you do NOT understand it well enough


微信图片_20220511102045.png

看到本期内容这么少,是不是心动了呢?


前言


上一篇万字长文 Java AQS队列同步器以及ReentrantLock的应用 为我们读 JUC 源码以及其设计思想做了足够多的铺垫,接下来的内容我将重点说明差异化,如果有些童鞋不是能很好的理解文中的一些内容,强烈建议回看上一篇文章,搞懂基础内容,接下来的阅读真会轻松加愉快


AQS 中我们介绍了独占式获取同步状态的多种情形:


  • 独占式获取锁


  • 可响应中断的独占式获取锁


  • 有超时限制的独占式获取锁


AQS 提供的模版方法里面还差共享式获取同步状态没有介绍,所以我们今天来揭开这个看似神秘的面纱


微信图片_20220511102132.png


AQS 中的共享式获取同步状态


独占式是你中没我,我中没你的的一种互斥形式,共享式显然就不是这样了,所以他们的唯一区别就是:


同一时刻能否有多个线程同时获取到同步状态


简单来说,就是这样滴:


微信图片_20220511102216.png


我们知道同步状态 state 是维护在 AQS 中的,抛开可重入锁的概念,我在上篇文章中也提到了,独占式和共享式控制同步状态 state 的区别仅仅是这样:


微信图片_20220511102238.png



所以说想了解 AQS 的 xxxShared 的模版方法,只需要知道它是怎么控制 state 的就好了


AQS共享式获取同步状态源码分析


为了帮助大家更好的回忆内容,我将上一篇文章的两个关键内容粘贴在此处,帮助大家快速回忆,关于共享式,大家只需要关注【骚紫色】就可以了


自定义同步器需要重写的方法


微信图片_20220511102333.png


AQS 提供的模版方法


微信图片_20220511102357.png


故事就从这里说起吧 (你会发现和独占式惊人的相似),关键代码都加了注释


    public final void acquireShared(int arg) {
          // 同样调用自定义同步器需要重写的方法,非阻塞式的尝试获取同步状态,如果结果小于零,则获取同步状态失败
        if (tryAcquireShared(arg) < 0)
              // 调用 AQS 提供的模版方法,进入等待队列
            doAcquireShared(arg);
    }


进入 doAcquireShared 方法:


    private void doAcquireShared(int arg) {
          // 创建共享节点「SHARED」,加到等待队列中
        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);
                      // 如果返回结果大于等于零,才能跳出外层循环返回
                    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);
        }
    }


上面代码第 18 行我们提到和独占式获取同步状态的区别,贴心的给大家一个更直观的对比:


微信图片_20220511102520.png


差别只在这里,所以我们就来看看 setHeadAndPropagate(node, r) 到底干了什么,我之前说过 JDK 源码中的方法命名绝大多数还是非常直观的,该方法直译过来就是 【设置头并且传播/繁衍】。独占式只是设置了头,共享式除了设置头还多了一个传播,你的疑问应该已经来了:


啥是传播,为什么会有传播这个设置呢?


想了解这个问题,你需要先知道非阻塞共享式获取同步状态返回值的含义:


微信图片_20220511102554.png


这里说的传播其实说的是 propagate > 0 的情况,道理也很简单,当前线程获取同步状态成功了,还有剩余的同步状态可用于其他线程获取,那就要通知在等待队列的线程,让他们尝试获取剩余的同步状态


如果要让等待队列中的线程获取到通知,需要线程调用 release 方法实现的。接下来,我们走近 setHeadAndPropagate 一探究竟,验证一下


  // 入参,node: 当前节点
    // 入参,propagate:获取同步状态的结果值,即上面方法中的变量 r
    private void setHeadAndPropagate(Node node, int propagate) {
            // 记录旧的头部节点,用于下面的check
        Node h = head; 
            // 将当前节点设置为头节点
        setHead(node);
            // 通过 propagate 的值和 waitStatus 的值来判断是否可以调用 doReleaseShared 方法
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
              // 如果后继节点为空或者后继节点为共享类型,则进行唤醒后继节点
                    // 这里后继节点为空意思是只剩下当前头节点了,另外这里的 s == null 也是判断空指针的标准写法
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }


上面方法的大方向作用我们了解了,但是代码中何时调用 doReleaseShared 的判断逻辑还是挺让人费解的,为什么会有这么一大堆的判断,我们来逐个分析一下:


这里的空判断有点让人头大,我们先挑出来说明一下:


微信图片_20220511102649.png

排除了其他判断条件的干扰,接下来我们就专注分析 propagate 和 waitStatus 两个判断条件就可以了,这里再将 waitStatus 的几种状态展示在这里,帮助大家理解,【骚粉色】是我们一会要用到的:


微信图片_20220511102710.png


propagate > 0


上面已经说过了,如果成立,直接短路后续判断,然后根据 doReleaseShared 的判断条件进行释放


propagate > 0 不成立, h.waitStatus < 0 成立 (注意这里的h是旧的头节点)


什么时候 h.waitStatus < 0 呢?抛开 CONDITION 的使用,只剩下 SIGNAL 和 PROPAGATE,想知道这个答案,需要提前看一下 doReleaseShared() 方法了:


    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                      // CAS 将头节点的状态设置为0                
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    // 设置成功后才能跳出循环唤醒头节点的下一个节点
                      unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         // 将头节点状态CAS设置成 PROPAGATE 状态
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }


doReleaseShared() 方法中可以看出:


  • 如果让 h.waitStatus < 0 成立,只能将其设置成 PROPAGATE = -3 的情况,设置成功的前提是 h 头节点 expected 的状态是 0;


  • 如果 h.waitStatus = 0,是上述代码第 8 行 CAS 设置成功,然后唤醒等待中的线程

所以猜测,当前线程执行到 h.waitStatus < 0 的判断前,有另外一个线程刚好执行了


doReleaseShared() 方法,将 waitStatus 又设置成PROPAGATE = -3


这个理解有点绕,我们还是来画个图理解一下吧:


微信图片_20220511102811.png


可能有同学还是不太能理解这么写的道理,我们一直说 propagate <> = 0 的情况,propagate = 0 代表的是当时/当时/当时 尝试获取同步状态没成功,但是之后可能又有共享状态被释放了,所以上面的逻辑是以防这种万一,你懂的,严谨的并发就是要防止一切万一,现在结合这个情景再来理解上面的判断你是否豁然开朗了呢?


继续向下看,


前序条件不成立,(h = head) == null || h.waitStatus < 0 注意这里的h是新的头节点)


有了上面铺垫,这个就直接画个图就更好理解啦,其实就是没有那么巧有另外一个线程掺合了


微信图片_20220511102904.png


相信到这里你应该理解共享式获取同步状态的全部过程了吧,至于非阻塞共享式获取同步状态带有超时时间获取同步状态,结合本文讲的 setHeadAndPropagate 逻辑和独占式获取同步状态的实现过程过程来看,真是一毛一样,这里就不再累述了,赶紧打开你的 IDE 去验证一下吧


我们分析了AQS 的模版方法,还一直没说 tryAcquireShared(arg) 这个方法是如何被重写的,想要了解这个,我们就来看一看共享式获取同步状态的经典应用 Semaphore


Semaphore 的应用及源码分析


Semaphore 概念


Semaphore 中文多翻译为 【信号量】,我还特意查了一下剑桥辞典的英文解释:



微信图片_20220511102950.png


其实就是信号标志(two flags),比如红绿灯,每个交通灯产生两种不同行为


  • Flag1-红灯:停车


  • Flag2-绿灯:行车


在 Semaphore 里面,什么时候是红灯,什么时候是绿灯,其实就是靠 tryAcquireShared(arg) 的结果来表示的


  • 获取不到共享状态,即为红灯


  • 获取到共享状态,即为绿灯


所以我们走近 Semaphore ,来看看它到底是怎么应用 AQS 的,又是怎样重写 tryAcquireShared(arg) 方法的


Semaphore 源码分析


先看一下类结构


微信图片_20220511103030.png


看到这里你是否有点跌眼镜,和 ReentrantLock 相似的可怕吧,如果你有些陌生,再次强烈建议你回看上一篇文章 Java AQS队列同步器以及ReentrantLock的应用 ,这里直接提速对比看公平和非公平两种重写的 tryAcquireShared(arg) 方法,没有意外,公平与否,就是判断是否有前驱节点


微信图片_20220511103117.png


方法内部只是计算 state 的剩余值,那 state 的初始值是多少怎么设置呢?当然也就是构造方法了:


        public Semaphore(int permits) {
          // 默认仍是非公平的同步器,至于为什么默认是非公平的,在上一篇文章中也特意说明过
        sync = new NonfairSync(permits);
    }
    NonfairSync(int permits) {
            super(permits);
    }


super 方法,就会将初始值给到 AQS 中的 state


微信图片_20220511103202.png


也许你发现了,当我们把 permits 设置为1 的时候,不就是 ReentrantLock 的互斥锁了嘛,说的一点也没错,我们用 Semaphore 也能实现基本互斥锁的效果


static int count;
//初始化信号量
static final Semaphore s 
    = new Semaphore(1);
//用信号量保证互斥    
static void addOne() {
  s.acquire();
  try {
    count+=1;
  } finally {
    s.release();
  }
}


But(英文听力中的重点),Semaphore 肯定不是为这种特例存在的,它是共享式获取同步状态的一种实现。如果使用信号量,我们通常会将 permits 设置成大于1的值,不知道你是否还记得我曾在 为什么要使用线程池? 一文中说到的池化概念,在同一时刻,允许多个线程使用连接池,每个连接被释放之前,不允许其他线程使用。所以说 Semaphore 可以允许多个线程访问一个临界区,最终很好的做到一个限流/限流/限流 的作用


虽然 Semaphore 能很好的提供限流作用,说实话,Semaphore 的限流作用比较单一,我在实际工作中使用 Semaphore 并不是很多,如果真的要用高性能限流器,Guava RateLimiter 是一个非常不错的选择,我们后面会做分析,有兴趣的可以提前了解一下

关于 Semaphore 源码,就这么三下五除二的结束了


微信图片_20220511103256.png


总结


不知你有没有感觉到,我们的节奏明显加快了,好多原来分散的点在被疯狂的串联起来,如果按照这个方式来阅读 JUC 源码,相信你也不会一头扎进去迷失方向,然后沮丧的退出 JUC 吧,然后面试背诵答案,然后忘记,然后再背诵?


跟上节奏,关于共享式获取同步状态,Semaphore 只不过是非常经典的应用,ReadWriteLock 和 CountDownLatch 日常应用还是非常广泛的,我们接下来就陆续聊聊它们吧


灵魂追问


  1. Semaphore 的 permits 设置成1 “等同于” 简单的互斥锁实现,那它和 ReentrantLock 的区别还是挺大的,都有哪些区别呢?


  1. 你在项目中是如何使用 Semaphore 的呢?



相关文章
|
20天前
|
移动开发 Java Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
【4月更文挑战第3天】在移动开发领域,性能优化一直是开发者关注的焦点。随着Kotlin的兴起,其在Android开发中的地位逐渐上升,但关于其与Java在性能方面的对比,尚无明确共识。本文通过深入分析并结合实际测试数据,探讨了Kotlin与Java在Android平台上的性能表现,揭示了在不同场景下两者的差异及其对应用性能的潜在影响,为开发者在选择编程语言时提供参考依据。
|
21天前
|
缓存 算法 Java
Java内存管理与调优:释放应用潜能的关键
【4月更文挑战第2天】Java内存管理关乎性能与稳定性。理解JVM内存结构,如堆和栈,是优化基础。内存泄漏是常见问题,需谨慎管理对象生命周期,并使用工具如VisualVM检测。有效字符串处理、选择合适数据结构和算法能提升效率。垃圾回收自动回收内存,但策略调整影响性能,如选择不同类型的垃圾回收器。其他优化包括调整堆大小、使用对象池和缓存。掌握这些技巧,开发者能优化应用,提升系统性能。
|
20天前
|
Java
深入理解Java并发编程:线程池的应用与优化
【4月更文挑战第3天】 在Java并发编程中,线程池是一种重要的资源管理工具,它能有效地控制和管理线程的数量,提高系统性能。本文将深入探讨Java线程池的工作原理、应用场景以及优化策略,帮助读者更好地理解和应用线程池。
|
27天前
|
Java 编译器 Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
在开发高性能的Android应用时,选择合适的编程语言至关重要。近年来,Kotlin因其简洁性和功能性受到开发者的青睐,但其性能是否与传统的Java相比有所不足?本文通过对比分析Kotlin与Java在Android平台上的运行效率,揭示二者在编译速度、运行时性能及资源消耗方面的具体差异,并探讨在实际项目中如何做出最佳选择。
17 4
|
28天前
|
数据采集 分布式计算 大数据
Java语言在大数据处理中的应用
传统的大数据处理往往依赖于庞大的数据中心和高性能的服务器,然而随着大数据时代的到来,Java作为一种强大的编程语言正在被广泛应用于大数据处理领域。本文将探讨Java语言在大数据处理中的优势和应用,以及其在分布式计算、数据处理和系统集成等方面的重要作用。
|
5天前
|
Java 关系型数据库 MySQL
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
UWB (ULTRA WIDE BAND, UWB) 技术是一种无线载波通讯技术,它不采用正弦载波,而是利用纳秒级的非正弦波窄脉冲传输数据,因此其所占的频谱范围很宽。一套UWB精确定位系统,最高定位精度可达10cm,具有高精度,高动态,高容量,低功耗的应用。
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
|
5天前
|
设计模式 算法 Java
Java中的设计模式及其应用
【4月更文挑战第18天】本文介绍了Java设计模式的重要性及分类,包括创建型、结构型和行为型模式。创建型模式如单例、工厂方法用于对象创建;结构型模式如适配器、组合关注对象组合;行为型模式如策略、观察者关注对象交互。文中还举例说明了单例模式在配置管理器中的应用,工厂方法在图形编辑器中的使用,以及策略模式在电商折扣计算中的实践。设计模式能提升代码可读性、可维护性和可扩展性,是Java开发者的必备知识。
|
5天前
|
安全 Java API
函数式编程在Java中的应用
【4月更文挑战第18天】本文介绍了函数式编程的核心概念,包括不可变性、纯函数、高阶函数和函数组合,并展示了Java 8如何通过Lambda表达式、Stream API、Optional类和函数式接口支持函数式编程。通过实际应用案例,阐述了函数式编程在集合处理、并发编程和错误处理中的应用。结论指出,函数式编程能提升Java代码的质量和可维护性,随着Java语言的演进,函数式特性将更加丰富。
|
6天前
|
Java API 数据库
深入解析:使用JPA进行Java对象关系映射的实践与应用
【4月更文挑战第17天】Java Persistence API (JPA) 是Java EE中的ORM规范,简化数据库操作,让开发者以面向对象方式处理数据,提高效率和代码可读性。它定义了Java对象与数据库表的映射,通过@Entity等注解标记实体类,如User类映射到users表。JPA提供持久化上下文和EntityManager,管理对象生命周期,支持Criteria API和JPQL进行数据库查询。同时,JPA包含事务管理功能,保证数据一致性。使用JPA能降低开发复杂性,但需根据项目需求灵活应用,结合框架如Spring Data JPA,进一步提升开发便捷性。
|
7天前
|
存储 缓存 安全
Java并发基础之互斥同步、非阻塞同步、指令重排与volatile
在Java中,多线程编程常常涉及到共享数据的访问,这时候就需要考虑线程安全问题。Java提供了多种机制来实现线程安全,其中包括互斥同步(Mutex Synchronization)、非阻塞同步(Non-blocking Synchronization)、以及volatile关键字等。 互斥同步(Mutex Synchronization) 互斥同步是一种基本的同步手段,它要求在任何时刻,只有一个线程可以执行某个方法或某个代码块,其他线程必须等待。Java中的synchronized关键字就是实现互斥同步的常用手段。当一个线程进入一个synchronized方法或代码块时,它需要先获得锁,如果
24 0