多线程学习笔记七之信号量Semaphore
简介
Semaphore[ˈseməfɔ:(r)]意为信号量,比较书面的解释是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
Semaphore维护了信号量许可,线程只有获得了许可才能够访问资源,可以把Semaphore理解为风景区管理员,风景区有人数限制,达到了人数限制管理员就会让后来的游客等着直到风景区里面的游客离开,这里风景区相当于需要协调的公共资源,人数限制就相当于Semaphore维护的许可量,而游客就相当于是执行任务的线程。
数据结构

Semaphore是基于共享锁实现的,内部类Sync是同步器AQS的子类,Sync有两个子类:公平信号量FairSync和非公平信号量NonefairSync,Semaphore默认非公平策略。
示例
public class SemaphoreTest {
private static final int THREAD_COUNT = 10;
private static ExecutorService pool = Executors.newFixedThreadPool(THREAD_COUNT);
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < THREAD_COUNT; i++) {
pool.execute(() -> {
try {
semaphore.acquire();
System.out.println("Thread " + Thread.currentThread().getId() + " is saving data");
Thread.sleep(1000);
System.out.println("Thread " + Thread.currentThread().getId() + " finished");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
pool.shutdown();
}
}
运行结果:

结果表明:同时只有三个线程能执行,因为Semaphore许可只有3个,相当于只有三个现场能访问同步资源,只有当线程释放许可,其他线程才能获取许可访问同步资源。
实现分析
构造方法
Semaphore提供两种构造函数,默认是非公平策略的,会根据传入的许可permits设置同步状态state。
public class Semaphore implements java.io.Serializable {
private static final long serialVersionUID = -3222578661600680210L;
/** All mechanics via AbstractQueuedSynchronizer subclass */
private final Sync sync;
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 1192457210091910933L;
//根据permits设置AQSstate
Sync(int permits) {
setState(permits);
}
final int getPermits() {
return getState();
}
}
}
信号量的获取(公平方式)
Semaphore提供的获取许可permits方法有acquire方法,都是调用的Sync分类AQS的acquireSharedInterruptibly方法,首先介绍基于公平策略如何获取信号量的。
//获取一个许可
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//获取多个许可
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
//AQS方法
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//有中断则抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//尝试获取“共享锁”;获取成功则直接返回,获取失败,则通过doAcquireSharedInterruptibly()获取。
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
tryAcquireShared(int acquires)
Sync子类FairSync实现的tryAcquireShared方法,首先判断AQS同步队列还有没有其他正在等待的线程,如果当前线程前面没有等待线程,尝试CAS修改,采用的是循环+CAS的方式修改同步状态
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())
return -1;
//目前还有多少许可
int available = getState();
//当前线程获得acquires个许可后剩下的许可
int remaining = available - acquires;
//剩下的许可大于0,CAS修改
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
doAcquireSharedInterruptibly(int arg)
如果尝试获取失败,就会调用AQS的doAcquireSharedInterruptibly方法,首先会以当前线程构成共享型Node节点加入同步队列尾部,如果上一个节点是head节点,就尝试获取共享锁,否则就进入等待状态,等待前继节点成为head节点释放共享锁并唤醒后继节点。
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 创建”当前线程“的Node节点,且Node中记录的锁是”共享锁“类型;并将该节点添加到AQS同步队列末尾。
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
//当前节点的上一个节点p
final Node p = node.predecessor();
//节点p是头节点就尝试修改同步状态
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 当前线程一直等待,直到获取到共享锁。
// 如果线程在等待过程中被中断过,则再次中断该线程(还原之前的中断状态)。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
信号量的释放(公平方式)
信号量的释放,本质上就是释放获取到的共享锁。与acquire方法对应,释放信号量也有两种release方法,都调用了AQS的releaseShared方法。
//释放一个许可
public void release() {
sync.releaseShared(1);
}
//释放多个许可
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
//AQS方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
tryReleaseShared(int releases)
tryReleaseShared方法是有内部类Sync提供实现的,意味着公平方式与非公平方式释放共享锁的实现相同的。循环+CAS修改同步状态。
protected final boolean tryReleaseShared(int releases) {
for (;;) {
//当前同步状态/许可数
int current = getState();
//释放了releases个许可后剩余的许可数
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
//CAS修改同步状态
if (compareAndSetState(current, next))
return true;
}
}
doReleaseShared()
tryReleaseShared成功释放后,doReleaseShared唤醒等待线程
private void doReleaseShared() {
for (;;) {
//头节点
Node h = head;
// 如果头节点不为null,并且头节点不等于tail节点。同步队列除了head节点还有其他等待节点
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
nonfairTryAcquireShared(int acquires)
非公平方式获取以及释放信号量的实现与公平方式只有tryAcquireShared的实现不同,释放的逻辑是相同的。
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
总结
基于共享锁实现的Semaphore可以控制一定数量的线程同时访问同步资源,超过数量的线程需要等待直到有线程完成操作释放许可,从而保证合理使用同步资源。
多线程学习笔记七之信号量Semaphore的更多相关文章
- Java IO学习笔记七:多路复用从单线程到多线程
作者:Grey 原文地址:Java IO学习笔记七:多路复用从单线程到多线程 在前面提到的多路复用的服务端代码中, 我们在处理读数据的同时,也处理了写事件: public void readHandl ...
- java多线程学习笔记——详细
一.线程类 1.新建状态(New):新创建了一个线程对象. 2.就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法.该状态的线程位于可运行线程池中, ...
- (转)Qt Model/View 学习笔记 (七)——Delegate类
Qt Model/View 学习笔记 (七) Delegate 类 概念 与MVC模式不同,model/view结构没有用于与用户交互的完全独立的组件.一般来讲, view负责把数据展示 给用户,也 ...
- Learning ROS for Robotics Programming Second Edition学习笔记(七) indigo PCL xtion pro live
中文译著已经出版,详情请参考:http://blog.csdn.net/ZhangRelay/article/category/6506865 Learning ROS forRobotics Pro ...
- JAVA多线程学习笔记(1)
JAVA多线程学习笔记(1) 由于笔者使用markdown格式书写,后续copy到blog可能存在格式不美观的问题,本文的.mk文件已经上传到个人的github,会进行同步更新.github传送门 一 ...
- Typescript 学习笔记七:泛型
中文网:https://www.tslang.cn/ 官网:http://www.typescriptlang.org/ 目录: Typescript 学习笔记一:介绍.安装.编译 Typescrip ...
- 多线程学习笔记九之ThreadLocal
目录 多线程学习笔记九之ThreadLocal 简介 类结构 源码分析 ThreadLocalMap set(T value) get() remove() 为什么ThreadLocalMap的键是W ...
- python3.4学习笔记(七) 学习网站博客推荐
python3.4学习笔记(七) 学习网站博客推荐 深入 Python 3http://sebug.net/paper/books/dive-into-python3/<深入 Python 3& ...
- Go语言学习笔记七: 函数
Go语言学习笔记七: 函数 Go语言有函数还有方法,神奇不.这有点像python了. 函数定义 func function_name( [parameter list] ) [return_types ...
随机推荐
- vue单页面应用项目优化总结(转载)
转载自:https://blog.csdn.net/qq_42221334/article/details/81907901这是之前在公司oa项目优化时罗列的优化点,基本都已经完成,当时花了点心思整理 ...
- 变量&常量
变量:variables 存储数据,以被后面的程序调用,可以看作是:装信息的容器: 变量的作用:(1)标记数据(2)存储数据 变量定义规范1.声明变量:定义变量 name = "Mr H ...
- 「Django」浏览+1的操作
适应于网页.文章等浏览次数统计 1.Models设置:添加viewed方法 class NewsTitle(models.Model): title = models.CharField(max_le ...
- map文件的使用
map文件相信大家并不陌生,大家都知道是用来调试的,但是具体怎么用你又清不清楚呢? 其实也很简单 1.拿JQ为例,我们需要备有jquery.js.jquery.min.js.jquery.min.ma ...
- Java基础-SSM之Spring和Mybatis以及Spring MVC整合案例
Java基础-SSM之Spring和Mybatis以及Spring MVC整合案例 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 能看到这篇文章的小伙伴,详细你已经有一定的Java ...
- Java基础-虚拟内存之映射字节缓冲区(MappedByteBuffer)
Java基础-虚拟内存之映射字节缓冲区(MappedByteBuffer) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.映射字节缓冲区 1>.什么是虚拟内存 答:虚拟内 ...
- win7下PLSQL Developer提示“ORA-12154: TNS:无法解析指定的连接标识符”
解决方法:卸载掉重新安装,注意安装的目录的文件夹不要有特殊的符号,例如64位系统的的安装目录会到Program Files (x86),这时候就会出现"ORA-12154: TNS:无法解析 ...
- ASP.NET乱码深度剖析
写在前面 在Web开发中,乱码应该算一个常客了.今天还好好的一个页面,第二天过来打开一看,中文字符全变“外星文”了.有时为了解决这样的问题,需要花上很长的时间去调试,直至抓狂,笔者也曾经历过这样的时期 ...
- decimal模块
简介 decimal意思为十进制,这个模块提供了十进制浮点运算支持. 常用方法 1.可以传递给Decimal整型或者字符串参数,但不能是浮点数据,因为浮点数据本身就不准确. 2.要从浮点数据转换为De ...
- 20155303 实验四 Android程序设计
20155303 实验四 Android程序设计 目录 第24章:初识Android 任务一: 完成Hello World, 要求修改res目录中的内容,Hello World后要显示自己的学号 学习 ...