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. sublime text 2应用

    http://www.sublimetext.com/2 可以下一个 a portable version  我自己的是win版和linux版都有 1.添加注释 先选择要注释的内容,然后按 ctrl ...

  2. UML——活动图

    宏观导图 是森马?   活动图,属于UML中动态建模工具图,它描述活动的顺序,展现从一个活动到另一个活动的控制流.活动图着重表现从一个活动到另一个活动的控制流,是内部处理驱动的流程.   其实,说白了 ...

  3. Linux-apache httd.conf文件详解

    Linux-apache httd.conf文件详解 # This is the main Apache server configuration file. It contains the # co ...

  4. Eureka详解系列(三)--探索Eureka强大的配置体系

    简介 通过前面的两篇博客,我们知道了:什么是 Eureka?为什么使用 Eureka?如何适用 Eureka?今天,我们开始来研究 Eureka 的源码,先从配置部分的源码开始看,其他部分后面再补充. ...

  5. Hive 中日志的存放位置

    目前hive启动无法成功,想查看下hive的日志定位问题,但发现hive的安装目录下并没有hive的日志,后来经过在网上谷歌发现: Hive中的日志分为两种 系统日志,记录了hive的运行情况,错误状 ...

  6. 2020 CCPC Wannafly Winter Camp Day1 C. 染色图

    2020 CCPC Wannafly Winter Camp Day1 C. 染色图 定义一张无向图 G=⟨V,E⟩ 是 k 可染色的当且仅当存在函数 f:V↦{1,2,⋯,k} 满足对于 G 中的任 ...

  7. Educational Codeforces Round 84 E. Count The Blocks

    传送门: 1327- E. Count The Blocks  题意:给你一个整数n,求10^n内(每个数有前导零)长度为1到n的块分别有多少个.块的含义是连续相同数字的长度. 题解:从n=1开始枚举 ...

  8. L2-013 红色警报 (25分) 并查集复杂度

    代码: 1 /* 2 这道题也是简单并查集,并查集复杂度: 3 空间复杂度为O(N),建立一个集合的时间复杂度为O(1),N次合并M查找的时间复杂度为O(M Alpha(N)), 4 这里Alpha是 ...

  9. Codeforces Round #650 (Div. 3) F1. Flying Sort (Easy Version) (离散化,贪心)

    题意:有一组数,每次操作可以将某个数移到头部或者尾部,问最少操作多少次使得这组数非递减. 题解:先离散化将每个数映射为排序后所对应的位置,然后贪心,求最长连续子序列的长度,那么最少的操作次数一定为\( ...

  10. Chrome Switchs & Chrome Pref

    Chrome Switchs: https://chromium.googlesource.com/chromium/src/+/master/chrome/common/chrome_switche ...