Semaphore概述及案例学习

Semaphore信号量用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理地使用公共资源。

public class SemaphoreTest {

    private static final int THREAD_COUNT = 30;
private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT); private static Semaphore s = new Semaphore(10); //10个许可证数量,最大并发数为10 public static void main(String[] args) {
for(int i = 0; i < THREAD_COUNT; i ++){ //执行30个线程
threadPool.execute(new Runnable() {
@Override
public void run() {
s.tryAcquire(); //尝试获取一个许可证
System.out.println("save data");
s.release(); //使用完之后归还许可证
}
});
}
threadPool.shutdown();
}
}
  • 创建一个大小为30的线程池,但是信号量规定在10,保证许可证数量为10。
  • 每次线程调用tryAcquire()或者acquire()方法都会原子性的递减许可证的数量,release()会原子性递增许可证数量。

类图结构及重要字段

public class Semaphore implements java.io.Serializable {
private static final long serialVersionUID = -3222578661600680210L;
/** All mechanics via AbstractQueuedSynchronizer subclass */
private final Sync sync; abstract static class Sync extends AbstractQueuedSynchronizer {
// permits指定初始化信号量个数
Sync(int permits) {
setState(permits);
}
// ...
} static final class NonfairSync extends Sync {...} static final class FairSync extends Sync {...} // 默认采用非公平策略
public Semaphore(int permits) {
sync = new NonfairSync(permits);
} // 可以指定公平策略
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
} //...
}
  • 基于AQS,类似于ReentrantLock,Sync继承自AQS,有公平策略和非公平策略两种实现。
  • 类似于CountDownLatch,state在这里也是通过构造器指定,表示初始化信号量的个数。

本篇文章阅读需要建立在一定的AQS基础之上,这边推荐几篇前置文章,可以瞅一眼:

void acquire()

调用该方法时,表示希望获取一个信号量资源,相当于acquire(1)

如果当前信号量个数大于0,CAS将当前信号量值减1,成功后直接返回。

如果当前信号量个数等于0,则当前线程将被置入AQS的阻塞队列。

该方法是响应中断的,其他线程调用了该线程的interrupt()方法,将会抛出中断异常返回。

    // Semaphore.java
public void acquire() throws InterruptedException {
// 传递的 arg 为 1 , 获取1个信号量资源
sync.acquireSharedInterruptibly(1);
}
// AQS.java
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 线程被 中断, 抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
// 子类实现, 公平和非公平两种策略
if (tryAcquireShared(arg) < 0)
// 如果获取失败, 则置入阻塞队列,
// 再次进行尝试, 尝试失败则挂起当前线程
doAcquireSharedInterruptibly(arg);
}

非公平

    static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L; NonfairSync(int permits) {
super(permits);
} protected int tryAcquireShared(int acquires) {
// 这里直接调用Sync定义的 非公平共享模式获取方法
return nonfairTryAcquireShared(acquires);
}
} abstract static class Sync extends AbstractQueuedSynchronizer { final int nonfairTryAcquireShared(int acquires) {
for (;;) {
// 获取当前信号量的值
int available = getState();
// 减去需要获取的值, 得到剩余的信号量个数
int remaining = available - acquires;
// 不剩了,表示当前信号量个数不能满足需求, 返回负数, 线程置入AQS阻塞
// 还有的剩, CAS设置当前信号量值为剩余值, 并返回剩余值
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}

你会发现,非公平策略是无法保证【AQS队列中阻塞的线程】和【当前线程】获取的顺序的,当前线程是有可能在排队的线程之前就拿到资源,产生插队现象。

公平策略就不一样了,它会通过hasQueuedPredecessors()方法看看队列中是否存在前驱节点,以保证公平性。

公平策略

    static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L; FairSync(int permits) {
super(permits);
} protected int tryAcquireShared(int acquires) {
for (;;) {
// 如果队列中在此之前已经有线程在排队了,直接放弃获取
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}

void acquire(int permits)

在acquire()的基础上,指定了获取信号量的数量permits。

    public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}

void acquireUninterruptibly()

该方法与acquire()类似,但是不响应中断。

    public void acquireUninterruptibly() {
sync.acquireShared(1);
} public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}

void acquireUninterruptibly(int permits)

该方法与acquire(permits)类似,但是不响应中断。

    public void acquireUninterruptibly(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireShared(permits);
}

boolean tryAcquire()

tryAcquire和acquire非公平策略公用一个逻辑,但是区别在于,如果获取信号量失败,或者CAS失败,将会直接返回false,而不会置入阻塞队列中。

一般try开头的方法的特点就是这样,尝试一下,成功是最好,失败也不至于被阻塞,而是立刻返回false。

    public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
abstract static class Sync extends AbstractQueuedSynchronizer {
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}

boolean tryAcquire(int permits)

相比于普通的tryAcquire(),指定了permits的值。

    public boolean tryAcquire(int permits) {
if (permits < 0) throw new IllegalArgumentException();
return sync.nonfairTryAcquireShared(permits) >= 0;
}

boolean tryAcquire(int permits, long timeout, TimeUnit unit)

相比于tryAcquire(int permits),增加了超时控制。

    public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}

void release()

将信号量值加1,如果有线程因为调用acquire方法而被阻塞在AQS阻塞队列中,将根据公平策略选择一个信号量个数满足需求的线程唤醒,线程唤醒后也会尝试获取新增的信号量。

参考文章:Java并发包源码学习系列:AQS共享模式获取与释放资源

    // Semaphore.java
public void release() {
sync.releaseShared(1);
}
// AQS.java
public final boolean releaseShared(int arg) {
// 尝试释放锁
if (tryReleaseShared(arg)) {
// 释放成功, 唤醒AQS队列里面最先挂起的线程
// https://blog.csdn.net/Sky_QiaoBa_Sum/article/details/112386838
doReleaseShared();
return true;
}
return false;
}
// Semaphore#Sync.java
abstract static class Sync extends AbstractQueuedSynchronizer {
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;
}
}
}

void release(int permits)

release()相比指定了permits的值。

    public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}

其他方法

Semaphore还提供其他一些方法,实现比较简单,这边就简单写一下吧:

    // 返回此信号量中当前可用的许可证数量, 其实就是得到当前的 state值  getState()
public int availablePermits() {
return sync.getPermits();
} // 将state更新为0, 返回0
public int drainPermits() {
return sync.drainPermits();
} // 减少reduction个许可证
protected void reducePermits(int reduction) {
if (reduction < 0) throw new IllegalArgumentException();
sync.reducePermits(reduction);
} // 判断公平策略
public boolean isFair() {
return sync instanceof FairSync;
} // 判断是否有线程证在等待获取许可证
public final boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
} // 返回正在等待获取许可证的线程数
public final int getQueueLength() {
return sync.getQueueLength();
} // 返回所有等待获取许可证的线程集合
protected Collection<Thread> getQueuedThreads() {
return sync.getQueuedThreads();
}

总结

Semaphore信号量用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理地使用公共资源。

  • 基于AQS,类似于ReentrantLock,Sync继承自AQS,有公平策略和非公平策略两种实现。
  • 类似于CountDownLatch,state在这里也是通过构造器指定,表示初始化信号量的个数。

每次线程调用tryAcquire()或者acquire()方法都会原子性的递减许可证的数量,release()会原子性递增许可证数量,只要有许可证就可以重复使用。

参考阅读

  • 《Java并发编程之美》
  • 《Java并发编程的艺术》

Java并发包源码学习系列:同步组件Semaphore源码解析的更多相关文章

  1. Java并发包源码学习系列:CLH同步队列及同步资源获取与释放

    目录 本篇学习目标 CLH队列的结构 资源获取 入队Node addWaiter(Node mode) 不断尝试Node enq(final Node node) boolean acquireQue ...

  2. Java并发包源码学习系列:同步组件CountDownLatch源码解析

    目录 CountDownLatch概述 使用案例与基本思路 类图与基本结构 void await() boolean await(long timeout, TimeUnit unit) void c ...

  3. Java并发包源码学习系列:同步组件CyclicBarrier源码解析

    目录 CyclicBarrier概述 案例学习 类图结构及重要字段 内部类Generation及相关方法 void reset() void breakBarrier() void nextGener ...

  4. Java并发包源码学习系列:AQS共享式与独占式获取与释放资源的区别

    目录 Java并发包源码学习系列:AQS共享模式获取与释放资源 独占式获取资源 void acquire(int arg) boolean acquireQueued(Node, int) 独占式释放 ...

  5. Java并发包源码学习系列:ReentrantLock可重入独占锁详解

    目录 基本用法介绍 继承体系 构造方法 state状态表示 获取锁 void lock()方法 NonfairSync FairSync 公平与非公平策略的差异 void lockInterrupti ...

  6. Java并发包源码学习系列:ReentrantReadWriteLock读写锁解析

    目录 ReadWriteLock读写锁概述 读写锁案例 ReentrantReadWriteLock架构总览 Sync重要字段及内部类表示 写锁的获取 void lock() boolean writ ...

  7. Java并发包源码学习系列:详解Condition条件队列、signal和await

    目录 Condition接口 AQS条件变量的支持之ConditionObject内部类 回顾AQS中的Node void await() 添加到条件队列 Node addConditionWaite ...

  8. Java并发包源码学习系列:挂起与唤醒线程LockSupport工具类

    目录 LockSupport概述 park与unpark相关方法 中断演示 blocker的作用 测试无blocker 测试带blocker JDK提供的demo 总结 参考阅读 系列传送门: Jav ...

  9. Java并发包源码学习系列:JDK1.8的ConcurrentHashMap源码解析

    目录 为什么要使用ConcurrentHashMap? ConcurrentHashMap的结构特点 Java8之前 Java8之后 基本常量 重要成员变量 构造方法 tableSizeFor put ...

随机推荐

  1. Spark DataSource Option 参数

    Spark DataSource Option 参数 1.parquet 2.orc 3.csv 4.text 5.jdbc 6.libsvm 7.image 8.json 9.xml 9.1读选项 ...

  2. Session (简介、、相关方法、流程解析、登录验证)

    Session简介 Session的由来 Cookie虽然在一定程度上解决了"保持状态"的需求,但是由于Cookie本身最大支持4096字节,以及Cookie本身保存在客户端,可能 ...

  3. Hadoop伪分布式环境搭建+Ubuntu:16.04+hadoop-2.6.0

    Hello,大家好 !下面就让我带大家一起来搭建hadoop伪分布式的环境吧!不足的地方请大家多交流.谢谢大家的支持 准备环境: 1, ubuntu系统,(我在16.04测试通过.其他版本请自行测试, ...

  4. 2019 第十届蓝桥杯大赛软件类省赛 Java A组 题解

    2019 第十届蓝桥杯大赛软件类省赛 Java A组 试题A 题解 ​ 题目最后一句贴心的提示选手应该使用 long (C/C++ 应该使用 long long). ​ 本题思路很直白,两重循环.外层 ...

  5. 前n项余数个数和

    一:O(n) 计算贡献:前n项中,能被i(1~n)整除的数的个数为(n/i)个,,也就是 i 给前n项中(n/i)个数做了余数 #include<iostream> using names ...

  6. codeforces #345 (Div. 1) D. Zip-line (线段树+最长上升子序列)

    Vasya has decided to build a zip-line on trees of a nearby forest. He wants the line to be as long a ...

  7. CodeForces - 948C (前缀和+二分)

    博客界面的小人搞不好导致无心写博客 题意:tyd非常喜欢玩雪,下雪下了n天,第i天她会堆一堆大小为Vi的雪堆,但因为天气原因,每堆雪会融化Ti,问每天总共融化了多少雪: 直接上代码+注释 1 #inc ...

  8. VJ train1 I-彼岸

    一道递推题(我这个菜鸡刚开始以为是排列组合) 题目: 突破蝙蝠的包围,yifenfei来到一处悬崖面前,悬崖彼岸就是前进的方向,好在现在的yifenfei已经学过御剑术,可御剑轻松飞过悬崖.现在的问题 ...

  9. Codeforces Global Round 11 C. The Hard Work of Paparazzi (DP)

    题意:有\(r\)X\(r\)的网格图,有\(n\)位名人,会在\(t_i\)时出现在\((x_i,y_i)\),如果过了\(t_i\)名人就会消失,从某一点走到另外一点需要花费的时间是它们之间的曼哈 ...

  10. AtCoder AIsing Programming Contest 2020 D - Anything Goes to Zero (二进制,模拟)

    题意:给你一个长度为\(n\)的\(01\)串,从高位到低位遍历,对该位取反,用得到的十进制数\(mod\)所有位上\(1\)的个数,不断循环直到为\(0\),输出每次遍历时循环的次数. 题解:根据题 ...