Semaphore详解

Semaphore基本使用场景

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

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

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

Semaphore - 信号量

下面是简单代码示范

public static void main(String[] args) {// 表示有2个许可.Semaphore sem = new Semaphore(2);for (int i = 0; i < 3; i++) {new Thread(() -> {try {// 默认使用一个许可.sem.acquire();System.out.println(Thread.currentThread() + " I get it.");TimeUnit.SECONDS.sleep(3);System.out.println(Thread.currentThread() + " I release it.");} catch (InterruptedException e) {e.printStackTrace();} finally {sem.release();}}).start();}
}

代码输出入下:

Thread[Thread-0,5,main] I get it.
Thread[Thread-1,5,main] I get it.
Thread[Thread-1,5,main] I release it.
Thread[Thread-0,5,main] I release it.
Thread[Thread-2,5,main] I get it.
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.

// arg 等于 1
public final void acquireSharedInterruptibly(int arg)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();// 关于tryAcquireShared,Semaphore有两种实现// 一种是公平锁,另一种是非公平锁. 这分析非公平锁.if (tryAcquireShared(arg) < 0)// 调用 AQS#doAcquireSharedInterruptibly(1) 方法doAcquireSharedInterruptibly(arg);
}

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

// NonfairSync#tryAcquireShared 方法.
// 注意: NonfairSync extends Sync !!!
protected int tryAcquireShared(int acquires) {return nonfairTryAcquireShared(acquires);
}// Sync#nonfairTryAcquireShared 方法
int nonfairTryAcquireShared(int acquires) {// 当多线程竞争比较激烈, 该for循环会进行多次.for (;;) {// 获取当前状态int available = getState();// 判断剩余允许线程int remaining = available - acquires;// 通过CAS保证多线程操作.// 最后返回剩余. 假设当前剩余2个. 要使用1个. // if执行(没有其他线程竞争)完成, 则最后返回1个.if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}
}

doAcquireSharedInterruptibly 方法

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

// 假设传入的参数为1.
private void doAcquireSharedInterruptibly(int arg)throws InterruptedException {// 将调用线程封装了共享型Node, 加入到双向链表的队尾final Node node = addWaiter(Node.SHARED);boolean failed = true;try {for (;;) {// 记录node的前任final Node p = node.predecessor();// 前任是头节点, 则尝试去获锁if (p == head) {int r = tryAcquireShared(arg);// 获锁成功,设置头节点,并且进行传播if (r >= 0) {setHeadAndPropagate(node, r);p.next = null; // help GCfailed = false;return;}}// 获锁失败, 判断是否进行睡眠, 若不睡眠就进行下次循环.if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}
}

获取锁流程总结

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 方法

// 释放共享锁
public final boolean releaseShared(int arg) {// 调用Semaphore#tryReleaseShared方法.if (tryReleaseShared(arg)) {// tryReleaseShared释放成功, 则释放双向链表中head的后继doReleaseShared();return true;}return false;
}

tryReleaseShared 方法

// Semaphore#tryReleaseShared
protected final boolean tryReleaseShared(int releases) {for (;;) {// 获取当前的许可, 有并发问题int current = getState();// 计算释放之后的许可数量int next = current + releases;if (next < current) // overflowthrow new Error("Maximum permit count exceeded");// 自旋(通过CAS)设置状态. 设置成功则返回true.if (compareAndSetState(current, next))return true;}
}

doReleaseShared 方法

private void doReleaseShared() {for (;;) {// 记录当前headNode h = head;// 队列中含有等待的节点if (h != null && h != tail) {// 记录头节点等待状态int ws = h.waitStatus;// 有下一个节点需要唤醒if (ws == Node.SIGNAL) {// CAS 设置状态, 若没有成功, 则是并发导致失败.if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))continue;// 唤醒后继.unparkSuccessor(h);}// 并发情况下,可能会出现wa为0,需要状态为PROPAGATE,保证唤醒else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue;}if (h == head)break;}
}

释放锁总结

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

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

图解Semaphore

public static void main(String[] args) throws InterruptedException {Semaphore sem = new Semaphore(2);for (int i = 0; i < 5; i++) {Thread thread = new Thread(() -> {try {sem.acquire();TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();} finally {sem.release();}});thread.start();}
}

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

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

日志第一部分

节点中的数字代表线程标识, 为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底层的共享锁模式

创作不易,感谢大家观看, 如果有疑问和发现文中错误, 欢迎留言探讨!


---------------------
作者:Wuv1Up
来源:CSDN
原文:https://blog.csdn.net/weixin_37150792/article/details/105692924
版权声明:本文为作者原创文章,转载请附上博文链接!
内容解析By:CSDN,CNBLOG博客文章一键转载插件

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

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

相关文章

PerfView专题 (第六篇):如何洞察 C# 中 GC 的变化

一&#xff1a;背景 在洞察 GC 方面&#xff0c;我觉得市面上没有任何一款工具可以和 PerfView 相提并论&#xff0c;这也是为什么我会在 WinDbg 之外还要学习这么一款工具的原因&#xff0c;这篇我们先简单聊聊 PerfView 到底能洞察 GC 什么东西&#xff1f;二&#xff1a;洞察…

Linux_日志管理介绍(一)

一、介绍1、CentOS 6.x中日志服务已经由rsyslogd取代了原先的syslogd服务&#xff0c;但是rsyslogd是和syslogd服务相兼容的2、除了系统默认的日志之外&#xff0c;采用RPM方式安装的系统服务也会默认把日志记录在/var/log/目录中&#xff08;源码包安装的服务日志是在源码包指…

如何将exe文件添加到开机启动

1、先创建exe文件的快捷方式 2、打开windows的startup启动目录&#xff08;针对win10以上&#xff09; windows有两个以上startup目录&#xff0c;一个是针对所有用户有效的&#xff0c;另外是每个用户下边有一个&#xff1a; 针对当前用户 &#xff1a; C:\Users\{当前用户}\A…

.NET MAUI 跨平台应用程序 (Windows App 和 Android )示例

也就前周&#xff0c;.Net MAUI正式版出来了 &#xff0c;一个支持跨平台的UI框架,Linux支持情况官网也没说&#xff0c;按理来说应该也是支持的&#xff0c;刚好&#xff0c;我最近也在研究GUI的基本原理&#xff0c;微软出品还是值得深入研究一下的&#xff0c;就先来个样例&…

OpenStack 计算节点删除

前提 计算节点中一个僵尸计算节点存在&#xff0c;而里面的CPU数目在总物理CPU中&#xff0c;导致认为当前能创建实例。而实际没有这么多资源。其中node-11为僵尸节点。 原因 删除计算节点不能直接格式化该服务器&#xff0c;否则在控制节点的数据库上会存在该计算节点的数据。…

PHP 7.2 新功能介绍

PHP 7.2 已經在 2017 年 11 月 30 日 正式發布 。這次發布包含新特性、功能&#xff0c;及優化&#xff0c;以讓我們寫出更好的代碼。在這篇文章裡&#xff0c;我將會介紹一些 PHP 7.2 最有趣的語言特性。 你可以在 Requests For Comments 頁面查看完整的更動清單。 核心改进 参…

如何打造单文件 Blazor Server 应用

前言上次&#xff0c;我们介绍了《如何打造单文件前后端集成 ASP.NET Core 应用》。但是&#xff0c;网友说&#xff0c;对于 Blazor Server 项目此方法无效。于是&#xff0c;我们测试了一下&#xff1a;BlazorApp1.csproj<Project Sdk"Microsoft.NET.Sdk.Web"&g…

Android线程池详解

引入线程池的好处 1&#xff09;提升性能。创建和消耗对象费时费CPU资源 2&#xff09;防止内存过度消耗。控制活动线程的数量&#xff0c;防止并发线程过多。 我们来看一下线程池的简单的构造 [html] view plaincopy print?public ThreadPoolExecutor(int corePoolSize, …

win11下vscode 自动升级失败 There was an error while marking a file for deletion

当升级vscode时出现下方报错&#xff1a; There was an error while marking a file for deletion:Failed to mark file for deletion:拒绝访问.Please verify there are no Visual Studio Code processes still executing既然是“拒绝访问”应该是权限问题&#xff0c;关闭vsc…

盘点大厂的那些开源项目 - 哔哩哔哩

哔哩哔哩现为中国年轻世代高度聚集的文化社区和视频平台&#xff0c;被粉丝们亲切地称为“B站”。overlord分类&#xff1a;缓存服务解决方案开发语言&#xff1a;GOOverlord是哔哩哔哩基于Go语言编写的memcache和redis&cluster的代理及集群管理功能&#xff0c;致力于提供…

单元测试,到底什么是单元测试,为什么单测这么难写

很多小伙伴想知道单测到底该怎么写&#xff0c;于是&#xff0c;文章就来了&#xff01; 话不多说&#xff0c;发车&#xff01; 来源于yes的练级攻略 &#xff0c;作者是Yes呀 到底什么是单元测试 这个问题看似非常简单&#xff0c;单元测试嘛&#xff0c;不就是咱们开发自己…

Linux 学习和教训

今天在学习Linux的时候&#xff0c;突然脑抽风&#xff0c;在根目录下执行了这样一条命令rm -rf *当时就觉得空气凝固了。。。那时也没有想到可以用数据恢复软件恢复。直接就重启了。重启之后发现&#xff0c;就去就直接是grub>晕菜。。突然间想到可以时候救援模式&#xff…

WinForm(八)窗体,窗体

我们在控件那篇文章里说过&#xff0c;窗体和控件都是一个类&#xff0c;项目中一个个窗体&#xff0c;都是Form类的子类。关于这个类有几个重要的成员&#xff0c;也是最常用成员&#xff0c;以供初学者了解&#xff1a;Load事件&#xff1a;发生在构造函数后&#xff0c;Show…

java8

实验总结 没问题 代码托管 https://git.oschina.net/shuoge/java8 转载于:https://www.cnblogs.com/haha-23333/p/6875325.html

抓包工具fiddler和wireshark对比

了解过网络安全技术的人都知道一个名词“抓包”。那对于局外人&#xff0c;一定会问什么是抓包&#xff1f;考虑到&#xff0c;大家的技术水平不一&#xff0c;我尽可能用非专业的口吻简单的说一下。 抓包就是将网络传输发送与接收的数据包进行截获、重发、编辑、转存等操作&am…

你被大数据“杀熟”过吗?怎么解决的?丨Q言Q语

点击关注 InfoQ&#xff0c;置顶公众号 接收程序员的技术早餐网友“廖师傅廖师傅”表示&#xff0c;他经常通过某网站订某个特定酒店的房间&#xff0c;长年价格在 380 元 -400 元。偶然一次&#xff0c;他从前台得知酒店淡季的价格在 300 元上下。他用朋友的账号查询也是 300 …

Blazor VS Vue

Vue——两分钟概述Vue 是一个JavaScript 框架。在其最简单的模式中&#xff0c;您可以简单地将核心 Vue 脚本包含在您的应用程序中&#xff0c;然后开始构建您的组件。除此之外&#xff0c;对于更复杂的应用程序&#xff0c;您可以使用 Vue 自己的 CLI 创建&#xff08;并最终发…

SAP ECC EHP7 RFC 发布成WebService

http://www.cnblogs.com/mingdashu/p/6877622.html 1、说明介绍 本文将RFC发布成WebService的详细步骤 不介绍如何创建rfc。 2、WebService创建 2.1、调用创建命令 在RFC界面点击 实用程序-->更多实用程序-->创建WEB服务-->来自函数模块 2.2、定义Web Service 2.2.1、…

一文把RabbitMQ讲透了,佩服!

目录 背景 消息队列 | 消息队列模式 ①点对点模式 ②发布/订阅模式 | 衡量标准 RabbitMQ 原理初探 | 基本概念 | 工作原理 | 常用交换器 | 消费原理 | 高级特性 ①过期时间 ②消息确认 ③持久化 ④死信队列 ⑤延迟队列 | 特性分析 RabbitMQ 环境搭建 Rabbi…

完美完全卸载Oracle 11g数据库

Oracle 11g可在开始菜单中卸载&#xff0c;然后同时需要删除注册表中相关内容。 操作系统&#xff1a;windows10专业版。 卸载步骤&#xff1a; 1、停用oracle服务&#xff1a;进入计算机管理&#xff0c;在服务中&#xff0c;找到oracle开头的所有服务&#xff0c;右击选择停止…