简介

  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的更多相关文章

  1. Java IO学习笔记七:多路复用从单线程到多线程

    作者:Grey 原文地址:Java IO学习笔记七:多路复用从单线程到多线程 在前面提到的多路复用的服务端代码中, 我们在处理读数据的同时,也处理了写事件: public void readHandl ...

  2. java多线程学习笔记——详细

    一.线程类  1.新建状态(New):新创建了一个线程对象.        2.就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法.该状态的线程位于可运行线程池中, ...

  3. (转)Qt Model/View 学习笔记 (七)——Delegate类

    Qt Model/View 学习笔记 (七) Delegate  类 概念 与MVC模式不同,model/view结构没有用于与用户交互的完全独立的组件.一般来讲, view负责把数据展示 给用户,也 ...

  4. Learning ROS for Robotics Programming Second Edition学习笔记(七) indigo PCL xtion pro live

    中文译著已经出版,详情请参考:http://blog.csdn.net/ZhangRelay/article/category/6506865 Learning ROS forRobotics Pro ...

  5. JAVA多线程学习笔记(1)

    JAVA多线程学习笔记(1) 由于笔者使用markdown格式书写,后续copy到blog可能存在格式不美观的问题,本文的.mk文件已经上传到个人的github,会进行同步更新.github传送门 一 ...

  6. Typescript 学习笔记七:泛型

    中文网:https://www.tslang.cn/ 官网:http://www.typescriptlang.org/ 目录: Typescript 学习笔记一:介绍.安装.编译 Typescrip ...

  7. 多线程学习笔记九之ThreadLocal

    目录 多线程学习笔记九之ThreadLocal 简介 类结构 源码分析 ThreadLocalMap set(T value) get() remove() 为什么ThreadLocalMap的键是W ...

  8. python3.4学习笔记(七) 学习网站博客推荐

    python3.4学习笔记(七) 学习网站博客推荐 深入 Python 3http://sebug.net/paper/books/dive-into-python3/<深入 Python 3& ...

  9. Go语言学习笔记七: 函数

    Go语言学习笔记七: 函数 Go语言有函数还有方法,神奇不.这有点像python了. 函数定义 func function_name( [parameter list] ) [return_types ...

随机推荐

  1. 域名、ip、以及通过域名访问网站、虚拟主机

    ip 是一个网站的id,是它的地址. 域名是为了解决ip比较难记住才引出的. 利用nginx来配置虚拟主机,通过域名可以访问该网站. 具体实现详见前面文章. 虚拟主机:可以实现在一台服务器虚拟出多个网 ...

  2. oracle:delete和truncate

    oracle中清空表数据的两种方法 1.delete from t 2 .truncate table t 区别: 1.delete是dml操作:truncate是ddl操作,ddl隐式提交不能回滚 ...

  3. 弹指之间 -- Prerequisites

    CHAPTER 1 吉他的分类 Electric Guitar Classic Guitar Folk Guitar CHAPTER 2 吉他各部名称 CHAPTER 3 选购吉他 琴颈弯曲程度 木头 ...

  4. arcgis创建渔网

    创建渔网 1.     ArcToolbox > Data Management Tools > Feature Class > Create Finshnet.选择输出要素位置,模 ...

  5. django 学习笔记(转)

    原文链接:https://my.oschina.net/linktime/blog/105280 例如有一下模型 from django.db import models class person(m ...

  6. vue父组件调用子组件资源

    通过上篇博文提到的方法我们可以触发子组件的某个事件来实现调用子组件的某些资源(例如数据和方法),但是更多的情况下我们会想不通过触发子组件的事件,而直接调用子组件的资源 这个时候我们就需要用到ref了, ...

  7. 【前端安全】JavaScript防XSS攻击

    什么是XSS XSS(Cross Site Scripting),跨站脚本攻击,是一种允许攻击者在另外一个用户的浏览器中执行恶意代码脚本的脚本注入式攻击.本来缩小应该是CSS,但为了和层叠样式(Cas ...

  8. bzoj千题计划292:bzoj2244: [SDOI2011]拦截导弹

    http://www.lydsy.com/JudgeOnline/problem.php?id=2244 每枚导弹成功拦截的概率 = 包含它的最长上升子序列个数/最长上升子序列总个数 pre_len ...

  9. bzoj千题计划105:bzoj3503: [Cqoi2014]和谐矩阵(高斯消元法解异或方程组)

    http://www.lydsy.com/JudgeOnline/problem.php?id=3503 b[i][j] 表示i对j是否有影响 高斯消元解异或方程组 bitset优化 #include ...

  10. 转:我是否该放弃VB.Net?

    我是否该放弃VB.Net呢?这个问题一次次的出现在我的脑海里,而且这种想法越来越强烈.放弃VB.Net至少能让我的生活变得轻松些.如果你是个C#程序员,那拷贝粘贴代码会很容易,因为可以找到的例子代码如 ...