目录

  • `Semaphore` 概述
  • `Semaphore` 源码
    • 类图
    • 构造器及内部类
    • 信号量的获取
      • 公平模式
      • 非公平模式
    • 信号量释放
  • `Semaphore` 应用示例
  • `Semaphore` 小结

Semaphore 概述

  • Semaphore 来自于 JDK 1.5JUC 包,直译过来就是 信号量,被作为一种多线程并发控制工具来使用
  • Semaphore 可以控制同时访问共享资源的线程个数,线程通过 acquire() 方法获取一个信号量,信号量减 1,如果没有就等待;通过 release() 方法释放一个信号量,信号量加 1
  • SemaphoreAPI 是这么介绍的:一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动

下面我们就一个停车场的简单例子来阐述 Semaphore

  • 我们假设停车场仅有 5 个停车位,一开始停车场没有车辆所有车位全部空着,然后先后到来三辆车,停车场车位够,安排进去停车,然后又来三辆,这个时候由于只有两个停车位,所有只能停两辆,其余一辆必须在外面候着,直到停车场有空车位,当然以后每来一辆都需要在外面候着。当停车场有车开出去,里面有空位了,则安排一辆车进去(至于是哪辆要看选择的机制是公平还是非公平模式
  • 从程序角度看,停车场就相当于信号量 Semaphore,其中许可数为 5,车辆就相对线程。当来一辆车时,许可数就会减 1,当停车场没有车位了(许可数 = 0 ),其他来的车辆需要在外面等候着。如果有一辆车开出停车场,许可数 +1,然后放进来一辆车

信号量 Semaphore 是一个非负整数(>= 1)。

  • 当一个线程想要访问某个共享资源时,它必须要先获取 Semaphore,当 Semaphore > 0 时,获取该资源并执行 Semaphore 减 1
  • 如果 Semaphore = 0,则表示 全部的共享资源已经被其他线程全部占用,线程必须要等待其他线程释放资源
  • 当其他线程释放资源时,执行 Semaphore + 1 操作

Semaphore 源码

类图


可以很明显的看出来 SemaphoreCountDownLatch 一样都是直接使用 AQS 实现的。区别就是 Semaphore 还分别实现了公平模式 FairSync 和非公平模式 NonfairSync 两个内部类

构造器及内部类

public class Semaphore implements java.io.Serializable {private static final long serialVersionUID = -3222578661600680210L;private final Sync sync;/*创建具有给定的信号量数和非公平模式的 Semaphore*/public Semaphore(int permits) {sync = new NonfairSync(permits);}/*创建具有给定的信号量数和给定的公平设置的 Semaphore*/public Semaphore(int permits, boolean fair) {sync = fair ? new FairSync(permits) : new NonfairSync(permits);}/*非公平模式的实现*/static final class NonfairSync extends Sync {private static final long serialVersionUID = -2694183684443567898L;NonfairSync(int permits) {super(permits);}//......}/*公平模式的实现*/static final class FairSync extends Sync {private static final long serialVersionUID = 2014338818796000944L;FairSync(int permits) {super(permits);}//......}/*信号量的同步实现。 使用 AQS 的state状态表示信号量。子分类为公平和非公平模式*/abstract static class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = 1192457210091910933L;Sync(int permits) {// 被设置为state值setState(permits);}//......}
}
  • 实际上公平与非公平只是在获取信号量的时候得到体现,它们的释放信号量的方法都是一样的,这就类似于 ReentrantLock:公平与非公平只是在获取锁的时候得到体现,它们的释放锁的方法都是一样的
  • 在构造器部分,如同 CountDownLatch 构造函数传递的初始化计数个数 count 被赋给了AQSstate 状态变量一样,Semaphore 的信号量个数 permits 同样赋给了AQSstate
  • 在创建 Semaphore 时可以使用一个 fair 变量指定是否使用公平策略,默认是非公平模式。公平模式会确保所有等待的获取信号量的线程按照先进先出的顺序获取信号量,而非公平模式则没有这个保证。非公平模式的吞吐量比公平模式的吞吐量要高,而公平模式则可以避免线程饥饿

信号量的获取

Semaphore 提供了 acquire() 方法来获取一个许可

public void acquire() throws InterruptedException {sync.acquireSharedInterruptibly(1);
}public void acquire(int permits) throws InterruptedException {if (permits < 0) throw new IllegalArgumentException();sync.acquireSharedInterruptibly(permits);
}

内部调用 AQSacquireSharedInterruptibly(int arg),该方法以共享模式获取同步状态

public final void acquireSharedInterruptibly(int arg)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();if (tryAcquireShared(arg) < 0)doAcquireSharedInterruptibly(arg);
}

acquireSharedInterruptibly(int arg)tryAcquireShared(int arg) 由子类来实现,对于 Semaphore 而言,如果我们选择非公平模式,则调用 NonfairSynctryAcquireShared(int arg) 方法,否则调用 FairSynctryAcquireShared(int arg) 方法

公平模式

内部类 NonfairSync 中的 tryAcquireShared() 方法

protected int tryAcquireShared(int acquires) {for (;;) {// 这是AQS实现公平模式的预定义的方法,AQS帮我们实现好了。该方法用于查询是否有任何线程等待获取信号量资源的时间超过当前线程// 如果该方法返回true,则表示有线程比当前线程更早地请求获取信号量资源。由于是公平的的,因此当前线程不应该获取信号量资源,直接返回-1,表示获取信号量资源失败if (hasQueuedPredecessors())return -1;// 获取当前的信号量许可int available = getState();// 设置“获得acquires个信号量许可之后,剩余的信号量许可数”int remaining = available - acquires;// CAS设置信号量if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}
}
  • 判断目前的信号量资源数量 state 的值,是否满足要获取的信号量资源数量,acquire() 方法默认获取 1 个资源。获取到了就是 CAS 的原子性的将 state 递减,否则表示获取资源失败,那么可能会阻塞。但是我们也会发现:如果 remaining >= 0,但是 CAS 更新 state 失败,那么会循环重试,这里为什么要重试呢?
  • 因为可能会有多个线程同时获取信号量资源,但是由于 CAS 只能保证一次只有一个线程成功,因此其他线程必定失败,但此时,实际上还是存在剩余的信号量资源没有被获取完毕的,因此让其他线程重试,相比于直接加入到同步队列中,对于信号量资源的利用率更高

非公平模式

对于非公平而言,因为它不需要判断当前线程是否位于 CLH 同步队列列头,所以相对而言会简单些

protected int tryAcquireShared(int acquires) {return nonfairTryAcquireShared(acquires);
}final int nonfairTryAcquireShared(int acquires) {/*开启一个循环尝试获取共享信号量资源*/for (;;) {// 获取state的值,我们知道state代表信号量资源数量int available = getState();int remaining = available - acquires;// 如果remaining < 0,那么返回remaining值,由于是负数,因此获取失败// 如果 >= 0,那么表示可以获取成功,尝试CAS的更新state,更新成功之后同样返回remaining,由于是大于等于0的数,因此获取成功if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;// 如果remaining大于等于0,但是CAS更新state失败,那么循环重试   }
}
  • 相比于公平模式的实现少了 hasQueuedPredecessors() 的判断。可以想象:如果某线程 A 先调用了 aquire() 方法获取信号量,但是如果当前信号量个数为 0,那么线程 A 会被放入 AQS 的同步队列阻塞
  • 过一段时间后线程 B 调用了 release() 方法释放了一个信号量,他它会唤醒队列中等待的线程 A,但是这时线程 C 又调用了 aquire() 方法。如果采用非公平策略,那么线程 C 就会和线程 A 去竞争这个信号量资源。由 nonfairTryAcquireShared() 的代码可知,线程 C 完全可以在线程 A 被激活前,或者激活后先于线程 A 获取到该信号量,也就是在这种模式下阻塞线程和当前请求的线程是竞争关系,而不遵循先来先得的策略

信号量释放

获取了许可,当用完之后就需要释放,Semaphore 提供 release() 来释放许可

public void release() {sync.releaseShared(1);
}public void release(int permits) {if (permits < 0) throw new IllegalArgumentException();sync.releaseShared(permits);
}

内部调用 AQSreleaseShared(int arg)

public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {// 释放成功,必定调用doReleaseShared尝试唤醒后继结点,即阻塞的线程doReleaseShared();return true;}return false;
}

同样 tryReleaseShared(arg)AQS 中的模板方法,由其子类实现。我们看 Semaphore 内部类 SynctryReleaseShared(int arg) 方法

protected final boolean tryReleaseShared(int releases) {for (;;) {int current = getState();// 信号量的许可数 = 当前信号许可数 + 待释放的信号许可数int next = current + releases;// 这里可以知道,信号量资源数量不可超过int的最大值if (next < current)throw new Error("Maximum permit count exceeded");// CAS增加state值,CAS成功之后返回true,否则循环重试if (compareAndSetState(current, next))return true;}
}

Semaphore 应用示例

我们以上述的停车为例

public class SemaphoreDemo {static class Parking {private Semaphore semaphore;public Parking(int count) {/*初始化Semaphore*/semaphore = new Semaphore(count);}public void park() {try {/*获取信号量,没获取一次,信号量减 1*/semaphore.acquire();long time = (long) (Math.random() * 10);System.out.println(Thread.currentThread().getName() + "进入停车场,停车" + time + "秒...");Thread.sleep(time);System.out.println(Thread.currentThread().getName() + "开出停车场...");} catch (InterruptedException e) {e.printStackTrace();} finally {/*释放信号量,每释放一次,信号量加 1*/semaphore.release();}}}static class Car extends Thread {private Parking parking;public Car(Parking parking) {this.parking = parking;}@Overridepublic void run() {/*进入停车场*/parking.park();}}public static void main(String[] args) {/*模拟停车位为 3*/Parking parking = new Parking(3);/*模拟有 5 辆车*/for (int i = 0; i < 5; i++) {new Car(parking).start();}}
}结果:
Thread-0进入停车场,停车4秒...
Thread-1进入停车场,停车2秒...
Thread-2进入停车场,停车9秒...
Thread-1开出停车场...
Thread-3进入停车场,停车5秒...
Thread-0开出停车场...
Thread-4进入停车场,停车2秒...
Thread-4开出停车场...
Thread-3开出停车场...
Thread-2开出停车场...

Semaphore 小结

  • SemaphoreCountDownLatch 的原理都差不多,都是直接使用 AQS共享模式 实现自己的逻辑,都是对于 AQSstate 资源的利用,但是它们却实现了不同的功能,CountDownLatchstate 被看作一个倒计数器,当 state 变为 0 时,表示被阻塞的线程可以放开执行。而 Semaphore 中的 state 被看作信号量资源,获取不到资源则可能会阻塞,获取到资源则可以访问共享区域,共享区域使用完毕要记得还回信号量
  • 很明显 Semaphore 的信号量资源很像锁资源,但是他们有不同,那就是 锁资源是和获得锁的线程绑定的,而这里的信号量资源并没有和线程绑定,也就是说你可以让一些线程不停的“释放信号量”,而另一些线程只是不停的“获取信号量”,这在 AQS 内部实际上就是对 state 状态的值的改变而已,与线程无关
  • 通常 Semaphore 可以用来控制多线程对于共享资源访问的并发量,在上面的案例中我们就见过!另外还需要注意的是,如果在 AQS 的同步队列中队头结点线程需要获取 n 个资源,目前有 m 个资源,如果 m 小于 n,那么这个队列中的头结点线程以及后面的所有结点线程都会因为不能获取到资源而继续阻塞,即使头结点后面的结点中的线程所需的资源数量小于 m 也不行。即已经在 AQS 同步队列中阻塞的线程,只能按照先进先出的顺序去获取资源,如果头部线程因为所需资源数量不够而一直阻塞,那么队列后面的线程必定不能获取资源

Semaphore源码解读相关推荐

  1. Semaphore 源码解读

    一.Semaphore Semaphore 通过设置一个固定数值的信号量,并发时线程通过 acquire() 获取一个信号量,如果能成功获得则可以继续执行,否则将阻塞等待,当某个线程使用 releas ...

  2. aqs java 简书,Java AQS源码解读

    1.先聊点别的 说实话,关于AQS的设计理念.实现.使用,我有打算写过一篇技术文章,但是在写完初稿后,发现掌握的还是模模糊糊的,模棱两可. 痛定思痛,脚踏实地重新再来一遍.这次以 Java 8源码为基 ...

  3. AQS同步器源码解读有感

    1.前言 AQS(AbstractQueuedSynchronizer)是java.util.concurrent的基础.也是Doug Lea大神为广大java开发作出的卓越贡献.J.U.C中的工具类 ...

  4. Bert系列(二)——源码解读之模型主体

    本篇文章主要是解读模型主体代码modeling.py.在阅读这篇文章之前希望读者们对bert的相关理论有一定的了解,尤其是transformer的结构原理,网上的资料很多,本文内容对原理部分就不做过多 ...

  5. Bert系列(三)——源码解读之Pre-train

    https://www.jianshu.com/p/22e462f01d8c pre-train是迁移学习的基础,虽然Google已经发布了各种预训练好的模型,而且因为资源消耗巨大,自己再预训练也不现 ...

  6. linux下free源码,linux命令free源码解读:Procps free.c

    linux命令free源码解读 linux命令free源码解读:Procps free.c 作者:isayme 发布时间:September 26, 2011 分类:Linux 我们讨论的是linux ...

  7. nodeJS之eventproxy源码解读

    1.源码缩影 !(function (name, definition) { var hasDefine = typeof define === 'function', //检查上下文环境是否为AMD ...

  8. PyTorch 源码解读之即时编译篇

    点击上方"AI遇见机器学习",选择"星标"公众号 重磅干货,第一时间送达 作者丨OpenMMLab 来源丨https://zhuanlan.zhihu.com/ ...

  9. Alamofire源码解读系列(九)之响应封装(Response)

    本篇主要带来Alamofire中Response的解读 前言 在每篇文章的前言部分,我都会把我认为的本篇最重要的内容提前讲一下.我更想同大家分享这些顶级框架在设计和编码层次究竟有哪些过人的地方?当然, ...

  10. Feflow 源码解读

    Feflow 源码解读 Feflow(Front-end flow)是腾讯IVWEB团队的前端工程化解决方案,致力于改善多类型项目的开发流程中的规范和非业务相关的问题,可以让开发者将绝大部分精力集中在 ...

最新文章

  1. php 判断是不是前一天,PHP开发中,定时执行如何判断之前的脚本是否跑完?
  2. pandas 遍历 series
  3. 如何固定最小宽度_第018期 安全疏散——疏散宽度
  4. wxWidgets:编写一个应用程序
  5. nginx rtmp代码架构1 hook点总结
  6. Sublime Text 3插件安装方法
  7. 异常:Caused by: java.lang.NoSuchMethodError: javax.persistence.OneToMany.orphanRemoval()Z
  8. 【小型JavaFx项目】英汉词典
  9. 【maven】maven pom文件详解
  10. 使用tcpdump抓Android网络包
  11. React 模板封装之基础模板 BaseTable
  12. permissionerror winerror 5 拒绝访问。
  13. 【目标跟踪】Long-term Correlation Tracking 阅读笔记
  14. 旅行社旅游APP开发维护经验
  15. 如何监控ActiveMQ
  16. 企业培训视频如何防止被下载和盗用?
  17. 你还不清楚某个系统文件的作用吗?Windows_xp系统文件详解【大全】
  18. 无线网络优化学习第一天
  19. 写在世界读书日 - 光读书不能让你成为供应链管理专家
  20. 阿里fastjson的用法

热门文章

  1. 算法:翻转整数7. Reverse Integer
  2. 【动手学深度学习】代码(持续更新)
  3. k8s和mysql怎么通信_k8s中的网络通信总结
  4. const数据成员的初始化
  5. 足球比赛两强相遇概率
  6. 通信系统设计中的凸优化问题
  7. 估计理论(1):最小方差无偏估计(第2章)
  8. AdaBoost中利用Haar特征进行人脸识别算法分析与总结2——级联分类器与检测过程
  9. 【 2019中国大学生程序设计竞赛(CCPC) - 网络选拔赛】1002.array【主席树】
  10. 【LaTeX安装】如何在windows电脑上安装 texlive2021