并发编程系列-Semaphore

Semaphore,如今通常被翻译为"信号量",过去也曾被翻译为"信号灯",因为类似于现实生活中的红绿灯,车辆是否能通行取决于是否是绿灯。同样,在编程世界中,线程是否能执行取决于信号量是否允许。

信号量是由著名的计算机科学家迪杰斯特拉(Dijkstra)于1965年提出的,直到1980年管程被提出,它一直是并发编程领域的主导方法。如今几乎所有支持并发编程的语言都支持信号量机制,因此掌握信号量仍然非常必要。

下面我们首先介绍信号量模型,然后介绍如何运用信号量,最后使用信号量来实现一个流量控制器。

信号量模型

信号量模型还是很简单的,可以简单概括为: 一个计数器,一个等待队列,三个方法。在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down()和up()。你可以结合下图来形象化地理解。

alt

这三个操作的具体意义如下所示。

  • 初始化(init()):设定计数器的起始值。
  • 减少(down()):将计数器的值减1;如果此时计数器的值小于0,则当前线程会被阻塞,否则当前线程可以继续执行。
  • 增加(up()):将计数器的值加1;如果此时计数器的值小于或等于0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。

上述提到的init()、down()和up()三个操作都是原子的,并且这种原子性由信号量模型的实现方保证。在Java SDK中,信号量模型由java.util.concurrent.Semaphore类实现,Semaphore类能够确保这三个操作都是原子操作。

如果你觉得上面的描述有些复杂,可以参考下面的代码来理解信号量模型的实现。

class Semaphore{
  // 计数器
  int count;
  // 等待队列
  Queue queue;
  // 初始化操作
  Semaphore(int c){
    this.count=c;
  }
  //
  void down(){
    this.count--;
    if(this.count<0){
      //将当前线程插入等待队列
      //阻塞当前线程
    }
  }
  void up(){
    this.count++;
    if(this.count<=0) {
      //移除等待队列中的某个线程T
      //唤醒线程T
    }
  }
}

这里再插一句,信号量模型里面,down()、up()这两个操作历史上最早称为P操作和V操作,所以信号量模型也被称为PV原语。另外,还有些人喜欢用semWait()和semSignal()来称呼它们,虽然叫法不同,但是语义都是相同的。在Java SDK并发包里,down()和up()对应的则是acquire()和release()。

如何使用信号量

通过前文,你应该会发现信号量的模型还是非常简单的,那具体应该如何使用呢?其实你可以想象一下红绿灯。在十字路口,红绿灯可以控制交通流量,这得益于一个重要的规则:车辆在通过路口前必须检查是否为绿灯,只有绿灯才能通行。这个规则与我们之前提到的锁规则很相似,不是吗?

实际上,信号量的使用也是类似的。让我们继续用累加器的例子来说明信号量的使用吧。在累加器的例子中,count += 1操作是一个临界区,只允许一个线程执行,也就是说需要保证互斥性。那么,在这种情况下,如何使用信号量来控制呢?

实际上非常简单,就像我们使用互斥锁一样,只需要在进入临界区之前执行一次down()操作,在退出临界区之前执行一次up()操作就可以了。下面是Java代码的示例,acquire()就是信号量中的down()操作,release()就是信号量中的up()操作。

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

接下来我们再来分析一下,信号量是如何确保互斥性的。假设有两个线程T1和T2同时访问addOne()方法,当它们同时调用acquire()时,由于acquire()是一个原子操作,因此只能有一个线程(假设是T1)将信号量中的计数器减为0,而另一个线程(T2)将计数器减为-1。对于线程T1来说,信号量中计数器的值是0,大于等于0,所以线程T1会继续执行;对于线程T2来说,信号量中计数器的值是-1,小于0,根据信号量模型中对down()操作的描述,线程T2将被阻塞。因此,此时只有线程T1可以进入临界区并执行 count += 1;

当线程T1执行release()操作,也就是up()操作时,信号量中计数器的值是-1,经过加1后的值是0,小于等于0,根据信号量模型中对up()操作的描述,此时等待队列中的T2将被唤醒。于是,在T1执行完临界区代码之后,T2才有机会进入临界区执行,从而确保了互斥性。

快速实现一个限流器

上述的示例,我们通过使用信号量实现了一个简单的互斥锁功能。也许你会觉得奇怪,既然Java的SDK中已经提供了Lock,为什么还需要提供Semaphore呢?实际上,互斥锁只是Semaphore的一部分功能,而Semaphore还有一个Lock无法实现的功能,那就是:允许多个线程访问临界区

实际情况中确实存在这样的需求。比较常见的例子是我们在工作中遇到的各种资源池,比如连接池、对象池、线程池等等。其中,你可能对数据库连接池最为熟悉,在同一时刻,允许多个线程同时使用连接池,但每个连接在释放之前不允许其他线程使用。

实际上,不久前我在工作中也遇到了一个对象池的需求。对象池指的是一次性创建N个对象,然后所有线程重复利用这些对象,当然在对象释放之前不允许其他线程使用。对象池可以使用List来保存实例对象,这很简单。但关键在于限流器的设计,这里的限流指的是不允许超过N个线程同时进入临界区。那么如何快速实现这样的限流器呢?我立刻想到了使用信号量的解决方案。

在上面的例子中,信号量的计数器被设置为1,这个1表示只允许一个线程进入临界区。但是,如果我们将计数器的值设为对象池中对象的数量N,就可以完美解决对象池的限流问题了。下面是一个对象池的示例代码。

class ObjPool<T, R> {
  final List<T> pool;
  // 用信号量实现限流器
  final Semaphore sem;
  // 构造函数
  ObjPool(int size, T t){
    pool = new Vector<T>(){};
    for(int i=0; i<size; i++){
      pool.add(t);
    }
    sem = new Semaphore(size);
  }
  // 利用对象池的对象,调用func
  R exec(Function<T,R> func) {
    T t = null;
    sem.acquire();
    try {
      t = pool.remove(0);
      return func.apply(t);
    } finally {
      pool.add(t);
      sem.release();
    }
  }
}
// 创建对象池
ObjPool<Long, String> pool =
  new ObjPool<Long, String>(10, 2);
// 通过对象池获取t,之后执行
pool.exec(t -> {
    System.out.println(t);
    return t.toString();
});

我们使用一个List来存储对象实例,并使用Semaphore实现限流器。关键代码位于ObjPool的exec()方法中,这个方法实现了限流的功能。在这个方法中,我们首先调用acquire()方法(在finally块中会调用release()方法),假设对象池大小为10,信号量的计数器初始化为10,那么前10个线程调用acquire()方法后可以继续执行,相当于通过了信号灯,而其他线程将阻塞在acquire()方法上。通过了信号灯的线程,我们为每个线程分配一个对象t(通过pool.remove(0)实现),并执行一个回调函数func,该回调函数的参数正是之前分配的对象t;在执行完回调函数后,它们会释放对象(通过pool.add(t)实现),同时调用release()方法来更新信号量的计数器。如果此时信号量的计数器值小于等于0,说明有线程在等待,此时会自动唤醒等待的线程。

简而言之,使用信号量,我们可以轻松实现一个限流器,且使用起来非常简单。

总结

信号量在Java语言中的知名度相对较低,但在其他编程语言中却非常有名。Java在并发编程领域取得了很快的发展,重点支持的是管程模型。管程模型在理论上解决了信号量模型的一些不足之处,主要体现在易用性和工程化方面。例如,使用信号量来解决我们前面提到的阻塞队列问题比使用管程模型要复杂许多。如果你感兴趣的话,可以进行了解和尝试。

本文由 mdnice 多平台发布

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

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

相关文章

8.10 用redis实现缓存功能和Spring Cache

什么是缓存? 缓存(Cache), 就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地代码。 通过Redis来缓存数据&#xff0c;减少数据库查询操作; 逻辑 每个分类的菜品保存一份缓存数据 数据库菜品数据有变更时清理缓存数据 如何将商品数据缓存起…

p-级数的上界(Upper bound of p-series)

积分判别法-The Integral Test https://math.stackexchange.com/questions/2858067/upper-bound-of-p-series https://courses.lumenlearning.com/calculus2/chapter/the-p-series-and-estimating-series-value/ 两个重要级数&#xff08;p级数和几何级数&#xff09; ht…

WPF显示初始界面--SplashScreen

WPF显示初始界面–SplashScreen 前言 WPF应用程序的运行速度快&#xff0c;但并不能在瞬间启动。当第一次启动应用程序时&#xff0c;会有一些延迟&#xff0c;因为公共语言运行时&#xff08;CLR&#xff09;首先需要初始化.NET环境&#xff0c;然后启动应用程序。 对于WPF中…

高忆管理:股票T+0交易是什么意思?t+0交易有什么好处?

股票的买卖准则有很多种&#xff0c;T0买卖便是其中之一。那么股票T0买卖是什么意思&#xff1f;t0买卖有什么优点&#xff1f;高忆管理也为大家预备了相关内容&#xff0c;以供参考。 股票T0买卖是什么意思&#xff1f; T0买卖准则是指出资者当天买入的股票能够在当天卖出&am…

SpringBoot 异步、邮件任务

异步任务 创建一个Hello项目 创建一个类AsyncService 异步处理还是非常常用的&#xff0c;比如我们在网站上发送邮件&#xff0c;后台会去发送邮件&#xff0c;此时前台会造成响应不动&#xff0c;直到邮件发送完毕&#xff0c;响应才会成功&#xff0c;所以我们一般会采用多线…

神经网络基础-神经网络补充概念-03-逻辑回归损失函数

概念 逻辑回归使用的损失函数通常是"对数损失"&#xff08;也称为"交叉熵损失"&#xff09;或"逻辑损失"。这些损失函数在训练过程中用于衡量模型预测与实际标签之间的差异&#xff0c;从而帮助模型逐步调整权重参数&#xff0c;以更好地拟合数…

c++--SLT六大组件之间的关系

1.SLT六大组件&#xff1a; 容器&#xff0c;迭代器&#xff0c;算法&#xff0c;仿函数&#xff0c;适配器&#xff0c;空间配置器 2.六大组件之间的关系 容器&#xff1a;容器是STL最基础的组件&#xff0c;没有容器&#xff0c;就没有数据&#xff0c;容器的作用就是用来存…

IO流 详细介绍

一、IO流概述 1.IO&#xff1a;输入(Input读取数据)/输出(Output写数据) 2.流&#xff1a;是一种抽象概念&#xff0c;是对数据传输的总称,也就是说数据在设备间的传输称为流&#xff0c;流的本质是数据传输IO流就是用来处理设备间数据传输问题的。 3.常见的应用&#xff1a…

JVM - 垃圾回收机制

JVM的垃圾回收机制(简称GC) JVM的垃圾回收机制非常强大&#xff0c;是JVM的一个很重要的功能&#xff0c;而且这也是跟对象实例息息相关的&#xff0c;如果对象实例不用了要怎么清除呢&#xff1f; 如何判断对象已经没用了 当JVM认为一个对像已经没用了&#xff0c;就会把这个…

初识Sentinel

目录 1.解决雪崩的方式有4种&#xff1a; 1.1.2超时处理&#xff1a; 1.1.3仓壁模式 1.1.4.断路器 1.1.5.限流 1.1.6.总结 1.2.服务保护技术对比 1.3.Sentinel介绍和安装 1.3.1.初识Sentinel 1.3.2.安装Sentinel 1.4.微服务整合Sentinel 2.流量控制 2.1.簇点链路 …

== 和 equals 的对比 [面试题]

和 equals 的对比[面试题] 文章目录 和 equals 的对比[面试题]1. 和 equals 简介2. Object 类中 equals() 源码3. String 类中 equals() 源码4. Integer 类中 equals() 源码5. 如何重写 equals 方法 1. 和 equals 简介 是一个比较运算符 &#xff1a;既可以判断基本数据类型…

【数据结构OJ题】链表的回文结构

原题链接&#xff1a;https://www.nowcoder.com/practice/d281619e4b3e4a60a2cc66ea32855bfa?tpId49&&tqId29370&rp1&ru/activity/oj&qru/ta/2016test/question-ranking 目录 1. 题目描述 2. 思路分析 3. 代码实现 1. 题目描述 2. 思路分析 在做这道…

老师如何制作二维码分班查询系统?技术老师分享的创建框架值得借鉴

作为一名班主任&#xff0c;开学前需要搞定分班问题&#xff0c;可以通过制作一个分班二维码查询系统&#xff0c;让学生和家长可以通过扫描二维码快速查到自己的分班信息&#xff0c;分享一下我制作的过程&#xff0c;希望对老师们有帮助&#xff08;结尾有惊喜&#xff09;&a…

内网穿透——使用Windows自带的网站程序建立网站

文章目录 1.前言2.Windows网页设置2.1 Windows IIS功能设置2.2 IIS网页访问测试 3. Cpolar内网穿透3.1 下载安装Cpolar3.2 Cpolar云端设置3.3 Cpolar本地设置 4.公网访问测试5.结语 1.前言 在网上各种教程和介绍中&#xff0c;搭建网页都会借助各种软件的帮助&#xff0c;比如…

RP2040开发板自制树莓派逻辑分析仪

目录 前言 1 准备工作和前提条件 1.1 Raspberry Pi Pico RP2040板子一个 1.2 Firmware-LogicAnalyzer-5.0.0.0-PICO.uf2固件 1.3 LogicAnalyzer-5.0.0.0-win-x64软件 2 操作指南 2.1 按住Raspberry Pi Pico开发板的BOOTSEL按键&#xff0c;再接上USB接口到电脑 2.2 刷入…

End-to-End Object Detection with Transformers

DERT 目标检测 基于卷积神经网络的目标检测回顾DETR对比Swin Transformer摘要检测网络流程DERT网络架构编码器概述解码器概述整体结构object queries的初始化Decoder中的Muiti-Head Self-AttentionDecoder中的Muiti-Head Attention 损失函数解决的问题 基于卷积神经网络的目标检…

内网穿透实战应用——【通过cpolar分享本地电脑上有趣的照片:发布piwigo网页】

通过cpolar分享本地电脑上有趣的照片&#xff1a;发布piwigo网页 文章目录 通过cpolar分享本地电脑上有趣的照片&#xff1a;发布piwigo网页前言1. 设定一条内网穿透数据隧道2. 与piwigo网站绑定3. 在创建隧道界面填写关键信息4. 隧道创建完成 总结 前言 首先在本地电脑上部署…

mysql知识点+面试总结

目录 1 mysql介绍 2 数据库常见语法 3 数据库表的常见语法 4 其他常见语法&#xff08;日期&#xff0c;查询表字段&#xff09; 5 JDBC开发步骤 6 索引 6.1 索引常见语法 7 常见面试总结 8 java代码搭建监控页面 1 mysql介绍 数据库&#xff1a;存储在硬盘上的文件系统…

VR虚拟展厅如何将客户引流到线下?

VR虚拟展厅是一项很不错的创新技术&#xff0c;将传统的展览内容以数字化形式呈现&#xff0c;为参观者带来全新的展示体验&#xff0c;也为企业带来了全新的宣传机遇。 线上虚拟展厅目前有着两种形式&#xff0c;一种是通过三维建模技术、虚拟现实技术等搭建的虚拟展厅&#x…

Docker 容器内无法使用vim命令 解决方法

目录 1. 问题所示2. 原理分析3. 解决方法1. 问题所示 进入Docker容器后 无法使用vim编辑器,出现如下问题:bash: vim: command not found 如图所示: 想着通过apt-get 安装vim,出现如下问题: root@b9f0fd330d5b:/# apt-get install vim Reading package lists... Done B…