前言

1965年,荷兰计算机科学家Dijkstra提出的信号量机制成为一种高效的进程同步机制。这之后的15年,信号量一直都是并发编程领域的终结者。1980年,管程被提出,成为继信号量之后的在并发编程领域的第二个选择。目前几乎所有的语言都支持信号量机制,Java也不例外。Java中提供了Semaphore并发工具类来支持信号量机制。下面我们就来了解Java实现的信号量机制。

首先介绍信号量模型,然后介绍如何使用,最后使用信号量来实现一个限流器。

信号量模型

信号量模型图(图来自参考[1]):

信号量模型总结为:一个计数器、一个等待队列和三个对外调用的方法。

计数器和等待队列时对外透明的,所有我们只能通过三个对外方法来访问计数器和等待队列。

init():设置计数器的初始值。

down():计数器的值减一。如果此时计数器的值小于0,则当前线程插入等待队列并阻塞,否则当前线程可以继续执行。

up():计数器的值加一。如果此时计数器的值小于或者等于0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。

这三个方法都是原子性的,由实现信号量模型的方法保证。在Java SDK中,信号量模型是由java.util.concurrent.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操作(荷兰语proberen,测试)和V操作(荷荷兰语verhogen,增加)。在我学的操作系统教材中(C语言实现),P操作对应wait(),V操作对应singal()。虽然叫法不同,但是语义都是相同的。在Java SDK并发包中,down()up()分别对应于Semaphore中的acquire()release()

如何使用信号量

信号量有时也被称为红绿灯,我们想想红绿灯时怎么控制交通的,就知道该如何使用信号量。车辆路过十字路时,需要先检查是否为绿灯,如果是则通行,否则就等待。想想和加锁机制有点相似,都是一样的操作,先检查是否符合条件(“尝试获取”),符合(“获取到”)则线程继续运行,否则阻塞线程。

下面使用累加器的例子来说明如何使用信号量。

count+=1 操作是个临界区,只允许一个线程执行,即要保证互斥。于是我们在进入临界区之前,使用down()即Java中的acquire(),在退出之后使用up()即Java中的release()。

static int count;
//初始化信号量
static final Semaphore s = new Semaphore(1); // 构造函数参数为1,表示只允许一个线程进行临界区。可实现一个互斥锁的功能。
//用信号量保证互斥
static void addOne() {
s.acquire(); // 获取一个许可(可看作加锁机制中加锁)
try {
count+=1;
} finally {
s.release(); // 归还许可(可看做加锁机制中解锁)
}
}

完整代码如下:

package com.sakura.concrrent;
import java.util.concurrent.Semaphore;
public class SemaphoreTest {
static int count;
static final Semaphore s = new Semaphore(1);
static void addOne() throws InterruptedException {
//只会有一个线程将信号量中的计数器减为1,而另外一个线程只能将信号量中计数器减为-1,导致被阻塞
s.acquire();  
try {
count +=1;
System.out.println("Now thread is " + Thread.currentThread() + "   and count is " + count);
}finally {
//进入临界区的线程在执行完临界区代码后将信号量中计数器的值加1然后,此时信号量中计数器的值为0,则从阻塞队列中唤醒被阻塞的进程
s.release();   
}
} public static void main(String[] args) {
// 创建两个线程运行
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread(); thread1.start();
thread2.start();
System.out.println("main thread"); }
}
class MyThread extends Thread{
@Override
public void run() {
super.run();
for(int i=0; i<10; i++) {                   
try {
SemaphoreTest.addOne();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

运行结果:

如果Semaphore的构造函数参数(许可数量,内置计数器的值)修改一下:

static final Semaphore s = new Semaphore(2);

计数器值的为2,那么就允许有两个线程进入临界区,我们的count值就会出现问题

快速实现一个限流器

当设置信号量的计数器为1时,可实现一个简单的互斥锁功能。但是,我们前面刚介绍过Java SDK中的Lock,Semaphore的用途显然不会与Lock一致,不然就重复造轮子了。Semaphore最重要的一个功能便是:可以允许多个线程访问一个临界区。(上述例子我们就设置了计数器的值为2,可发现thread1和thread2都可进入临界区。)

我们会在什么地方遇见这种需求呢?

各种池化资源,例如连接池、对象池、线程池等等。例如,数据库连接池,在同一时刻,一定是允许多个线程同时使用连接池,当然,每个连接在被释放之前,是不允许其他线程使用的。

我们设计如下可以允许N个线程使用的对象池,我们将信号量的计数器值设为N,就可以让N个线程同时进行临界区,多余的就会被阻塞。(代码来自参考[1])

class ObjPool<T, R> {
final List<T> pool;    //使用List保存实例对象
// 用信号量实现限流器
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();    //允许N个进程同时进入临界区
try {
//我们需要注意,因为多个进行可以进入临界区,所以Vector的remove方法是线程安全的
t = pool.remove(0);    
return func.apply(t);    //获取对象池汇中的一个对象后,调用func函数
} finally {
pool.add(t);    //离开临界区之前,将之前获取的对象放回到池中
sem.release();    //使得计数器加1,如果信号量中计数器小于等于0,那么说明有线程在等待,此时就会自动唤醒等待线程
}
}
}
// 创建对象池
ObjPool<Long, String> pool = new ObjPool<Long, String>(10, 2); // 通过对象池获取 t,之后执行  
pool.exec(t -> {
System.out.println(t);
return t.toString();
});

小结

记得学习操作系统时,信号量类型分为了好几种整型信号量、记录型信号量、AND信号量以及“信号量集”(具体了解可戳参考[2])。我认为Java SDK中Semaphore应该是记录型信号量的实现。不由想起,编程语言是对OS层面操作的一种抽象描述。这句话需要品需要细细品。

参考:

[1] 极客时间专栏王宝令《Java并发编程实战》

[2] 静水深流.操作系统之信号量机制总结.https://www.cnblogs.com/IamJiangXiaoKun/p/9464336.html

【Java并发工具类】Semaphore的更多相关文章

  1. Java并发工具类Semaphore应用实例

    package com.thread.test.thread; import java.util.Random; import java.util.concurrent.*; /** * Semaph ...

  2. 25.大白话说java并发工具类-CountDownLatch,CyclicBarrier,Semaphore,Exchanger

    1. 倒计时器CountDownLatch 在多线程协作完成业务功能时,有时候需要等待其他多个线程完成任务之后,主线程才能继续往下执行业务功能,在这种的业务场景下,通常可以使用Thread类的join ...

  3. 基于AQS实现的Java并发工具类

    本文主要介绍一下基于AQS实现的Java并发工具类的作用,然后简单谈一下该工具类的实现原理.其实都是AQS的相关知识,只不过在AQS上包装了一下而已.本文也是基于您在有AQS的相关知识基础上,进行讲解 ...

  4. 30行自己写并发工具类(Semaphore, CyclicBarrier, CountDownLatch)是什么体验?

    30行自己写并发工具类(Semaphore, CyclicBarrier, CountDownLatch)是什么体验? 前言 在本篇文章当中首先给大家介绍三个工具Semaphore, CyclicBa ...

  5. Java并发工具类 - CountDownLatch

    Java并发工具类 - CountDownLatch 1.简介 CountDownLatch是Java1.5之后引入的Java并发工具类,放在java.util.concurrent包下面 http: ...

  6. Java并发工具类CountDownLatch源码中的例子

    Java并发工具类CountDownLatch源码中的例子 实例一 原文描述 /** * <p><b>Sample usage:</b> Here is a pai ...

  7. java 并发工具类CountDownLatch & CyclicBarrier

    一起在java1.5被引入的并发工具类还有CountDownLatch.CyclicBarrier.Semaphore.ConcurrentHashMap和BlockingQueue,它们都存在于ja ...

  8. JAVA并发工具类---------------(CountDownLatch和CyclicBarrier)

    CountDownLatch是什么 CountDownLatch,英文翻译为倒计时锁存器,是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待. 闭锁可以延迟线程的进 ...

  9. 【Java并发工具类】Java并发容器

    前言 Java并发包有很大一部分都是关于并发容器的.Java在5.0版本之前线程安全的容器称之为同步容器.同步容器实现线程安全的方式:是将每个公有方法都使用synchronized修饰,保证每次只有一 ...

随机推荐

  1. React Native 性能优化指南【全网最全,值得收藏】

    2020 年谈 React Native,在日新月异的前端圈,可能算比较另类了.文章动笔之前我也犹豫过,但是想到写技术文章又不是赶时髦,啥新潮写啥,所以还是动笔写了这篇 React Native 性能 ...

  2. Python3-Selenium自动化测试框架(二)之selenium使用和元素定位

    Selenium自动化测试框架(二)之selenium使用和元素定位 (一)selenium的简单使用 1.导包 from selenium import webdriver 2.初始化浏览器 # 驱 ...

  3. mqtt实现跨平台跨应用通讯

    介绍 最近物联网应用一直很火,也打算做一些这方面的尝试,就边学边做在家花了2天时间做了一个简单demo,功能很简单,使用emq x 作为mqtt broker,用python写了一个定时抓取主机CPU ...

  4. linux死机解决办法

    linux死机后不要长按电源建强制关机,容易对损坏系统配置或者电脑硬件,导致重启后产生不必要的麻烦 如果是在图形界面下死机的话,不要再依赖任何图形界面的工具,解决方法如下: 1.一种方式是进入终端界面 ...

  5. Asp.Net Core 3.0 Kestrel服务器下 高性能 WebSocket Server

    最近研究.net core 的各种高性能类型,内存池之类的东西,基于kestrel 服务器的websocket ,写个例子练下手 把原生的Websocket用ArrayPool<T>,Me ...

  6. 数据量不足,MedicalNet 如何助力医疗影像 AI 突破瓶颈?

    ​导读 |近日,云+社区技术沙龙“腾讯开源技术”圆满落幕.本次沙龙邀请了多位腾讯技术专家,深度揭秘了腾讯开源项目TencentOS tiny.TubeMQ.Kona JDK.TARS以及Medical ...

  7. Nginx作为web静态资源服务器——跨域访问

    跨站访问 ​ 为什么浏览器禁止跨域访问 ​ Nginx跨站访问 Syntax:add_header name value [always]; Default:—— Context:http,serve ...

  8. 洛谷 UVA1395 苗条的生成树 Slim Span

    题目链接 题目描述 求所有生成树中最大边权与最小边权差最小的,输出它们的差值. 题目分析 要求所有生成树中边权极差最小值,起初令人无从下手.但既然要求所有生成树中边权极差最小值,我们自然需要对每一棵生 ...

  9. 认识一下 RabbitMQ

    分布式系统中,如何在各个应用之间高效的进行通信,是系统设计中的一个关键. 使用 消息代理(message broker) 是一个优雅的解决方案. RabbitMQ 就是一个被广泛应用的消息代理,遵循 ...

  10. Java入门 - 语言基础 - 10.条件语句

    原文地址:http://www.work100.net/training/java-if-else.html 更多教程:光束云 - 免费课程 条件语句 序号 文内章节 视频 1 概述 2 if...e ...