Semaphore详解

news2024/12/23 23:52:32

Semaphore的基本使用场景是限制一定数量的线程能够去执行.

举个简单的例子: 一个单向隧道能同时容纳10个小汽车或5个卡车通过(1个卡车等效与2个小汽车), 而隧道入口记录着当前已经在隧道内的汽车等效比重. 比如1个小汽车和1个卡车, 则隧道入口显示3. 若隧道入口显示10表示已经满了. 当汽车驶出隧道之后, 隧道入口显示的数字则会相应的减小. 于这个示例相符合场景非常适合用信号量.

Semaphore在构造的时候, 可以传入一个int. 表示有多少许可(permit). 线程获取锁的时候, 要告诉信号量使用多少许可(类比与小汽车和卡车), 当线程要使用的许可不足时, 则调用的线程则会被阻塞. 可以和上面简单的举例进行初步理解.

Semaphore - 信号量

下面是简单代码示范

 
  1. public static void main(String[] args) {

  2. // 表示有2个许可.

  3. Semaphore sem = new Semaphore(2);

  4. for (int i = 0; i < 3; i++) {

  5. new Thread(() -> {

  6. try {

  7. // 默认使用一个许可.

  8. sem.acquire();

  9. System.out.println(Thread.currentThread() + " I get it.");

  10. TimeUnit.SECONDS.sleep(3);

  11. System.out.println(Thread.currentThread() + " I release it.");

  12. } catch (InterruptedException e) {

  13. e.printStackTrace();

  14. } finally {

  15. sem.release();

  16. }

  17. }).start();

  18. }

  19. }

代码输出入下:

 
  1. Thread[Thread-0,5,main] I get it.

  2. Thread[Thread-1,5,main] I get it.

  3. Thread[Thread-1,5,main] I release it.

  4. Thread[Thread-0,5,main] I release it.

  5. Thread[Thread-2,5,main] I get it.

  6. Thread[Thread-2,5,main] I release it.

上述大致可以分为以下三步:

  1. 第一步: 首先线程0和1, 获取锁. 线程3被被阻塞.
  2. 第二步: 3秒过后, 线程0和线程1分别释放锁,
  3. 第三步: 线程2可以获得到锁.

Semaphore获取锁流程

Semaphore可以有4个方式获得锁.

获取锁的方式

  1. acquire() 线程占用一个许可.
  2. acquire(int) 线程占用int个许可
  3. acquireUninterruptibly() 线程占用一个许可, 调用不可以打断
  4. acquireUninterruptibliy(int) 线程占用int个许可,调用并且不可打断

4个方法只有细微的不同, 这里用 acquire() 用来分析, 其他的可以自行分析.

acquire 方法

调用Semaphore#acquire() 方法, 它本质上是调用的AQS#acquireSharedInterruptibly(int), 参数为1.

 
  1. // arg 等于 1

  2. public final void acquireSharedInterruptibly(int arg)

  3. throws InterruptedException {

  4. if (Thread.interrupted())

  5. throw new InterruptedException();

  6. // 关于tryAcquireShared,Semaphore有两种实现

  7. // 一种是公平锁,另一种是非公平锁. 这分析非公平锁.

  8. if (tryAcquireShared(arg) < 0)

  9. // 调用 AQS#doAcquireSharedInterruptibly(1) 方法

  10. doAcquireSharedInterruptibly(arg);

  11. }

上面代码中, 因为AQS规定tryAcquireShared方法要由实现方覆写. 所以在Semaphore中存在两个覆写, 一个是公平锁的覆写, 另一个是非公平锁的覆写. 这里选择以非公平锁来阅读. 因为日常使用较多(可能是无意识的,构造方法只需要传入一个int).

 
  1. // NonfairSync#tryAcquireShared 方法.

  2. // 注意: NonfairSync extends Sync !!!

  3. protected int tryAcquireShared(int acquires) {

  4. return nonfairTryAcquireShared(acquires);

  5. }

  6. // Sync#nonfairTryAcquireShared 方法

  7. int nonfairTryAcquireShared(int acquires) {

  8. // 当多线程竞争比较激烈, 该for循环会进行多次.

  9. for (;;) {

  10. // 获取当前状态

  11. int available = getState();

  12. // 判断剩余允许线程

  13. int remaining = available - acquires;

  14. // 通过CAS保证多线程操作.

  15. // 最后返回剩余. 假设当前剩余2个. 要使用1个.

  16. // if执行(没有其他线程竞争)完成, 则最后返回1个.

  17. if (remaining < 0 ||

  18. compareAndSetState(available, remaining))

  19. return remaining;

  20. }

  21. }

doAcquireSharedInterruptibly 方法

假设上面方法 getState() 方法返回0, 期望使用1个, 则计算得到remaining = -1, 则最后返回-1. 因此会进入到下面的方法doAcquireSharedInterruptibly(int)

 
  1. // 假设传入的参数为1.

  2. private void doAcquireSharedInterruptibly(int arg)

  3. throws InterruptedException {

  4. // 将调用线程封装了共享型Node, 加入到双向链表的队尾

  5. final Node node = addWaiter(Node.SHARED);

  6. boolean failed = true;

  7. try {

  8. for (;;) {

  9. // 记录node的前任

  10. final Node p = node.predecessor();

  11. // 前任是头节点, 则尝试去获锁

  12. if (p == head) {

  13. int r = tryAcquireShared(arg);

  14. // 获锁成功,设置头节点,并且进行传播

  15. if (r >= 0) {

  16. setHeadAndPropagate(node, r);

  17. p.next = null; // help GC

  18. failed = false;

  19. return;

  20. }

  21. }

  22. // 获锁失败, 判断是否进行睡眠, 若不睡眠就进行下次循环.

  23. if (shouldParkAfterFailedAcquire(p, node) &&

  24. parkAndCheckInterrupt())

  25. throw new InterruptedException();

  26. }

  27. } finally {

  28. if (failed)

  29. cancelAcquire(node);

  30. }

  31. }

获取锁流程总结

Semaphore获取锁的过程总结为如下:

  1. 判断是否满足获取锁条件, 关键方法nonfairTryAcquireShared.
  2. 若获取锁成功,则也会修改state.
  3. 若获取锁失败,关键方法doAcquireSharedInterruptibly阻塞的获取锁.
    1. 添加到双向链表
    2. 若是头节点后继, 则尝试获取锁, 否者则判断进入睡眠等待唤醒, 唤醒后继续执行3.2
    3. 若不进入睡眠,则直接运行到3.2步

Semaphore释放锁流程

Semaphore释放锁两个方法.
Semaphore释放锁的方法

  1. release() 释放一个许可
  2. release(int) 释放int个许可

该两个方法都会调用AQS#releaseShared(int)方法, 使用release()方法,则参数为1, 使用release(int)方法, 则参数为int.

releaseShared 方法

 
  1. // 释放共享锁

  2. public final boolean releaseShared(int arg) {

  3. // 调用Semaphore#tryReleaseShared方法.

  4. if (tryReleaseShared(arg)) {

  5. // tryReleaseShared释放成功, 则释放双向链表中head的后继

  6. doReleaseShared();

  7. return true;

  8. }

  9. return false;

  10. }

tryReleaseShared 方法

 
  1. // Semaphore#tryReleaseShared

  2. protected final boolean tryReleaseShared(int releases) {

  3. for (;;) {

  4. // 获取当前的许可, 有并发问题

  5. int current = getState();

  6. // 计算释放之后的许可数量

  7. int next = current + releases;

  8. if (next < current) // overflow

  9. throw new Error("Maximum permit count exceeded");

  10. // 自旋(通过CAS)设置状态. 设置成功则返回true.

  11. if (compareAndSetState(current, next))

  12. return true;

  13. }

  14. }

doReleaseShared 方法

 
  1. private void doReleaseShared() {

  2. for (;;) {

  3. // 记录当前head

  4. Node h = head;

  5. // 队列中含有等待的节点

  6. if (h != null && h != tail) {

  7. // 记录头节点等待状态

  8. int ws = h.waitStatus;

  9. // 有下一个节点需要唤醒

  10. if (ws == Node.SIGNAL) {

  11. // CAS 设置状态, 若没有成功, 则是并发导致失败.

  12. if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))

  13. continue;

  14. // 唤醒后继.

  15. unparkSuccessor(h);

  16. }

  17. // 并发情况下,可能会出现wa为0,需要状态为PROPAGATE,保证唤醒

  18. else if (ws == 0 &&

  19. !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))

  20. continue;

  21. }

  22. if (h == head)

  23. break;

  24. }

  25. }

释放锁总结

Semaphore释放锁的过程总结为如下:

  1. 释放N个许可, 因为存在并发释放, 需要CAS确保设置更新后的值.
  2. 唤醒双向链表中有效的等待节点. (可能存在并发问题,引入PROPAGATE状态)
  3. 被唤醒的节点调用获取锁的流程.

图解Semaphore

 
  1. public static void main(String[] args) throws InterruptedException {

  2. Semaphore sem = new Semaphore(2);

  3. for (int i = 0; i < 5; i++) {

  4. Thread thread = new Thread(() -> {

  5. try {

  6. sem.acquire();

  7. TimeUnit.SECONDS.sleep(5);

  8. } catch (InterruptedException e) {

  9. e.printStackTrace();

  10. } finally {

  11. sem.release();

  12. }

  13. });

  14. thread.start();

  15. }

  16. }

上面的程序, 通过添加不暂停断点输出日志信息来查看全部流程. 将输出拆分, 方便查看.

日志的第一部分:
日志第一部分

日志第一部分

节点中的数字代表线程标识, 为x表示没有记录线程. 最右边为抢到锁的节点, 里面记录的是线程号.

日志第二部分:
日志第二部分

  1. 由于 线程0 和 线程1 开始释放锁, 并且都更新state的状态
  2. 线程0 和 线程1 同时去唤醒队列中下一个有效节点, 存在并发问题
  3. 线程0成功唤醒, 设置线程2的节点的waitStatus为0, 线程1去唤醒, 发现有人已经设置过, 所以设置线程2的节点的waitStatus为PROPAGATE(传播).
  4. 线程0唤醒完毕,退出释放锁方法. 线程2抢锁成功, 并且线程1也随之退出释放锁的方法.

日志第二部分

线程2抢锁成功之后的图:
线程2抢锁成功

日志第三部分:
日志第三部分

  1. 线程2唤醒线程3的节点, 线程2唤醒任务结束.
  2. 线程3成功获取锁, 线程3去唤醒线程4. 线程3唤醒任务结束.

第三部分

日志第四部分:
日志第四部分

  1. 线程4尝试获得锁, 最后失败.
  2. 判断能否进入睡眠, 发现前任的waitStatus没有设置成SIGNAL, 因此不能睡眠, 再次尝试.
  3. 尝试失败, 进入睡眠.

第四部分

日志第五部分:
日志第五部分

  1. 线程2和线程3依次释放锁, 并且唤醒队列中下一个线程.
  2. 线程2 唤醒 线程4, 线程4去抢锁, 线程2唤醒任务结束,退出释放锁方法.
  3. 线程4尝试抢锁, 发现抢锁成功(后续还需要设置在队列中的状态等,所以并不是最终完成).
  4. 线程3由于线程2修改了头节点, 因此线程3设置头节点状态为PROPAGATE.
  5. 线程4和3唤醒任务结束.

第五部分

第五部分执行结束后:
第五部分结束后的状态

  1. 线程4获取锁.
  2. 头节点因此线程3和线程2并发唤醒队列中的线程,导致线程3第一次失败, 而第二次修改的时候,线程4已经将头节头改变, 但是碰巧列表中已经没有等待的节点,所以头节点的waitStatus为0, 因此线程3将头节点的waitStatus设置为PROPAGATE.
  3. 线程4获取锁后, 会将封装线程4的节点中的线程置为null, 方便为GC回收.

日志第六层部分:
第六部分日志

  1. 线程4释放锁, 进入doReleaseShared方法, 发现队列中已经没有节点.

因此AQS中最后的队列就下图所示:
第六部分

结束语

  1. 了解Semaphore锁的释放和获取流程
  2. 了解Semaphore的底层逻辑
  3. 了解AQS底层的共享锁模式

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/464238.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《string的模拟实现》

本文主要介绍库里面string类的模拟实现 文章目录 前言一、string的构造函数①无参的构造函数②带参的构造函数③修改构造函数 二、析构函数三、拷贝构造四、赋值重载五、返回size 、capacity和empty六、[]的运算符重载七、迭代器① 正向迭代器。② 正向const迭代器 八、string比…

PointPillars点云编码器代码运行过程中的问题及解决

PointPillars:点云编码器&#xff0c;编码特征可以与任何标准的 2D 卷积检测架构一起使用。任务是目标检测。来自CVPR2019 论文地址&#xff1a;https://arxiv.org/pdf/1812.05784.pdf 代码地址&#xff1a;GitHub - nutonomy/second.pytorch: PointPillars for KITTI object…

【LeetCode】297. 二叉树的序列化与反序列化

1.问题 序列化是将一个数据结构或者对象转换为连续的比特位的操作&#xff0c;进而可以将转换后的数据存储在一个文件或者内存中&#xff0c;同时也可以通过网络传输到另一个计算机环境&#xff0c;采取相反方式重构得到原数据。 请设计一个算法来实现二叉树的序列化与反序列…

Css如何优雅的实现抽奖转盘

如图&#xff0c;抽奖转盘&#xff0c;可以拆分为几部分&#xff1a; 1.底部大圆&#xff1b; 2.中间小圆&#xff1b; 3.扇形区&#xff1b; 4.扇形内部奖品区&#xff1b; 5.抽奖按钮&#xff1b; 6.点击抽奖按钮时旋转动效及逻辑&#xff1b; 这其中&#xff0c;扇形区&am…

集成灶/小家电语音提示芯片方案-WTN6040-8S唯创知音自主研发

集成灶一直是厨房中常用设备之一&#xff0c;而现代技术的不断发展&#xff0c;为集成灶的升级提供了更多的可能性。深圳唯创知音为了让家用电器更加便民&#xff0c;专门为集成灶开发了一款语音IC方案——WTN6040语音芯片方案&#xff0c;这款芯片可以满足集成灶对语音提示功能…

独家专访丨TheStage.ai :当 AI 邂逅 Web3

随着2022年末ChatGPT走红&#xff0c;AI再次成为人们关注的焦点。上一次AI掀起舆论热潮&#xff0c;还是2016年AlphaGo以4:1战胜世界顶级围棋棋手李世石。 但与上次不同的是&#xff0c;这次AI更强大了。在接受强化训练后&#xff0c;AI可以对用户的需求创造新内容。 面对AI的快…

关于API数据接口的使用说明

API&#xff08;Application Programming Interface&#xff09;是一种让不同软件之间进行数据交换和通信的技术&#xff0c;使用API可以减少开发者的工作量&#xff0c;提高软件应用的效率和可靠性。本文将介绍API数据接口的使用方式和注意事项。 使用API数据接口的方式&…

Python一行代码实现文件共享【内网穿透公网访问】

目录 1. 前言 2. 视频教程 3. 本地文件服务器搭建 3.1 python的安装和设置 3.2 cpolar的安装和注册 4. 本地文件服务器的发布 4.1 Cpolar云端设置 4.2 Cpolar本地设置 5. 公网访问测试 6. 结语 转载自内网穿透工具的文章&#xff1a;Python一行代码实现文件共享【内网…

PPOCRV3文本识别模型精度损失问题解决

PPOCRV3文本识别模型精度损失问题解决 1. 得到可用的ncnn模型2. 先看问题3. 快速解决4. 问题分析5. 最终效果6. 结语 1. 得到可用的ncnn模型 paddleocr文本识别模型(ch_PPOCRv3_rec_infer)转ncnn模型&#xff0c;我参考了这位大神的博客&#xff0c;基本包括了我遇到的所有问题…

操作系统八股文知识点汇总

1. 程序编译过程 gcc HelloWorld.c -E -o HelloWorld.i 预处理&#xff1a;加入头文件&#xff0c;替换宏。gcc HelloWorld.c -S -c -o HelloWorld.s 编译&#xff1a;包含预处理&#xff0c;将 C 程序转换成汇编程序。gcc HelloWorld.c -c -o HelloWorld.o 汇编&#xff1a;包…

在linux下搭建clash服务

下载clash并配置 clash安装包 一般下载名称中带clash-linux-amd64的包 下载完用gunzip解压&#xff0c;解压后重命名或者链接到系统环境变量目录都行 下载配置信息 wget -O config.yaml [订阅链接] wget -O Country.mmdb https://www.sub-speeder.com/client-download/Coun…

深元边缘计算盒子在社区的应用,提高社区的安全性和生活质量

近年来&#xff0c;随着人工智能技术的不断发展和普及&#xff0c;越来越多的社区开始应用边缘计算盒子AI视觉分析技术&#xff0c;以提高社区的安全性和管理效率。本文将介绍边缘计算盒子AI视觉分析技术在社区中的应用及其优势。 一、边缘计算盒子AI视觉在社区中的应用 1.安防…

uniapp中实现自定义导航栏

整个小程序默认配置存在系统内置导航和tabbar&#xff0c;项目中需求存在自定义的导航。 uniapp中vue封装组件&#xff08;顶部导航、底部tabbar&#xff09;&#xff0c;按照vue的相关语法使用。 在page.json文件中修改配置&#xff1a; 自定义导航组件&#xff1a; 给自定义…

PLM听过很多遍,却依旧不知道是什么?看完这篇你就懂

上周参加展会&#xff0c;很多客户在现场了解到e企拆图解决方案后&#xff0c;向我们咨询了很多问题&#xff0c;发现有几个名词经常被提及&#xff0c;比如PLM、PDM、BOM等。随着技术的爆炸发展&#xff0c;新的名词概念也与日俱增&#xff0c;对于这些名词&#xff0c;可能我…

工贸企业重大事故隐患判定标准,自2023年5月15日起施行

应急管理部发布了《工贸企业重大事故隐患判定标准》&#xff08;自2023年5月15日起施行&#xff09;&#xff0c;适用于判定冶金、有色、建材、机械、轻工、纺织、烟草、商贸等工贸企业重大事故隐患。新修改的安全生产法对建立健全重大事故隐患治理督办制度、督促生产经营单位消…

关于ffmpeg的使用过程中遇到的点(php)

有段日子没更新&#xff0c;最近使用ffmepg&#xff0c;这里记录一下 我这边就直说一下我工作中遇到的注意事项和使用方法&#xff0c;就不太详细说了 首先是安装的问题&#xff0c;windwos的话比较简单&#xff0c;官网下载安装文件&#xff0c;解压之后。设置环境变量 系统…

【react全家桶学习】react中组件定义及state属性(超详/必看)

函数式组件定义及特点 定义&#xff08;核心就是一个函数&#xff0c;返回虚拟dom&#xff09;&#xff1a; import React from reactexport default function index() {return <div>index</div> }特点&#xff1a; 1、适用于【简单组件】的定义2、是一个函数&a…

【三维重建】NeRF原理+代码讲解

文章目录 一、技术原理1.概览2.基于神经辐射场&#xff08;Neural Radiance Field&#xff09;的体素渲染算法3.体素渲染算法4.位置信息编码&#xff08;Positional encoding&#xff09;5.多层级体素采样 二、代码讲解1.数据读入2.创建nerf1.计算焦距focal与其他设置2.get_emb…

1690_Python中的复数数据类型

全部学习汇总&#xff1a;GreyZhang/python_basic: My learning notes about python. (github.com) 之前总结的知识中设计的数据类型有整形、浮点、字符串等&#xff0c;这些类型表示的都是一个单独的独立数据对象。在Python有也有表示复数改变的数据类型&#xff0c;也就是下…

Gradio入门到进阶全网最详细教程[二]:快速搭建AI算法可视化部署演示(侧重参数详解和案例实践)

常用的两款AI可视化交互应用比较&#xff1a; Gradio Gradio的优势在于易用性&#xff0c;代码结构相比Streamlit简单&#xff0c;只需简单定义输入和输出接口即可快速构建简单的交互页面&#xff0c;更轻松部署模型。适合场景相对简单&#xff0c;想要快速部署应用的开发者。 …