简介
Semaphore称为计数信号量,它允许n个任务同时访问某个资源,可以将信号量看做是在向外分发使用资源的许可证,只有成功获取许可证,才能使用资源。
Semaphore,即信号量,它保存了一系列的许可(permits),每次调用acquire()都将消耗一个许可,每次调用release()都将归还一个许可。
Semaphore的数据结构
分析源码可以知道,Semaphore底层是基于AbstractQueuedSynchronizer来实现的,所以,Semaphore的数据结构也依托于AQS的数据结构,在前面对AQS的分析中已经指出了其数据结构,在这里不再累赘。
Semaphore源码分析
类的继承关系
public class Semaphore implements java.io.Serializable {}
说明:Semaphore实现了Serializable接口,即可以进行序列化。
类的内部类
Semaphore总共有三个内部类,并且三个内部类是紧密相关的,下面先看三个类的关系。
说明:Semaphore与ReentrantLock的内部类的结构相同,类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。下面逐个进行分析。
Sync类
Sync类的源码如下。
/**
* Synchronization implementation for semaphore. Uses AQS state
* to represent permits. Subclassed into fair and nonfair
* versions.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 1192457210091910933L;
Sync(int permits) {
setState(permits);
}
final int getPermits() {
return getState();
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
final void reducePermits(int reductions) {
for (;;) {
int current = getState();
int next = current - reductions;
if (next > current) // underflow
throw new Error("Permit count underflow");
if (compareAndSetState(current, next))
return;
}
}
final int drainPermits() {
for (;;) {
int current = getState();
if (current == 0 || compareAndSetState(current, 0))
return current;
}
}
}
说明:Sync类的属性相对简单,只有一个版本号,Sync类存在如下方法和作用如下。
通过Sync的几个实现方法,我们获取到以下几点信息:
(1)许可是在构造方法时传入的;
(2)许可存放在状态变量state中;
(3)尝试获取一个许可的时候,则state的值减1;
(4)当state的值为0的时候,则无法再获取许可;
(5)释放一个许可的时候,则state的值加1;
(6)许可的个数可以动态改变;
NonfairSync类
NonfairSync类继承了Sync类,表示采用非公平策略获取资源,其只有一个tryAcquireShared方法,重写了AQS的该方法,其源码如下。
/**
* NonFair version
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
说明:从tryAcquireShared方法的源码可知,其会调用父类Sync的nonfairTryAcquireShared方法,表示按照非公平策略进行资源的获取。
FairSync类
FairSync类继承了Sync类,表示采用公平策略获取资源,其只有一个tryAcquireShared方法,重写了AQS的该方法,其源码如下。
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;
}
}
}
说明:从tryAcquireShared方法的源码可知,它使用公平策略来获取资源,它会判断同步队列中是否存在其他的等待节点。
类的属性
private static final long serialVersionUID = -3222578661600680210L;
/** All mechanics via AbstractQueuedSynchronizer subclass */
private final Sync sync;
说明:Semaphore自身只有两个属性,最重要的是sync属性,基于Semaphore对象的操作绝大多数都转移到了对sync的操作。
类的构造函数
Semaphore(int)型构造函数
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
说明:该构造函数会创建具有给定的许可数和非公平的公平设置的Semaphore。
Semaphore(int, boolean)型构造函数
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
说明:该构造函数会创建具有给定的许可数和给定的公平设置的Semaphore。
核心方法分析
acquire()方法
此方法从信号量获取一个(多个)许可,在提供一个许可前一直将线程阻塞,或者线程被中断,其源码如下
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
说明:该方法中将会调用Sync对象的acquireSharedInterruptibly(从AQS继承而来的方法)方法。获取一个许可,默认使用的是可中断方式,如果尝试获取许可失败,会进入AQS的队列中排队。
最终可以获取大致的方法调用序列(假设使用非公平策略)。如下图所示。
说明:上图只是给出了大体会调用到的方法,和具体的示例可能会有些差别,之后会根据具体的示例进行分析。
release()方法
此方法释放一个(多个)许可,将其返回给信号量,源码如下。
public void release() {
sync.releaseShared(1);
}
说明:该方法中将会调用Sync对象的releaseShared(从AQS继承而来的方法)方法。释放一个许可,释放一个许可时state的值会加1,并且会唤醒下一个等待获取许可的线程。
最终可以获取大致的方法调用序列(假设使用非公平策略)。如下图所示。
说明:上图只是给出了大体会调用到的方法,和具体的示例可能会有些差别,之后会根据具体的示例进行分析。
acquireUninterruptibly()方法
public void acquireUninterruptibly() {
sync.acquireShared(1);
}
获取一个许可,非中断方式,如果尝试获取许可失败,会进入AQS的队列中排队。
tryAcquire()方法
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
尝试获取一个许可,使用Sync的非公平模式尝试获取许可方法,不论是否获取到许可都返回,只尝试一次,不会进入队列排队。
tryAcquire(long timeout, TimeUnit unit)方法
public boolean tryAcquire(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
尝试获取一个许可,先尝试一次获取许可,如果失败则会等待timeout时间,这段时间内都没有获取到许可,则返回false,否则返回true;
acquire(int permits)方法
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
一次获取多个许可,可中断方式。
acquireUninterruptibly(int permits)方法
public void acquireUninterruptibly(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireShared(permits);
}
一次获取多个许可,非中断方式。
tryAcquire(int permits)方法
public boolean tryAcquire(int permits) {
if (permits < 0) throw new IllegalArgumentException();
return sync.nonfairTryAcquireShared(permits) >= 0;
}
一次尝试获取多个许可,只尝试一次。
tryAcquire(int permits, long timeout, TimeUnit unit)方法
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}
尝试获取多个许可,并会等待timeout时间,这段时间没获取到许可则返回false,否则返回true。
release(int permits)方法
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
一次释放多个许可,state的值会相应增加permits的数量。
availablePermits()方法
public int availablePermits() {
return sync.getPermits();
}
获取可用的许可次数。
drainPermits()方法
public int drainPermits() {
return sync.drainPermits();
}
销毁当前可用的许可次数,对于已经获取的许可没有影响,会把当前剩余的许可全部销毁。
reducePermits(int reduction)方法
protected void reducePermits(int reduction) {
if (reduction < 0) throw new IllegalArgumentException();
sync.reducePermits(reduction);
}
减少许可的次数。
四、示例
下面给出了一个使用Semaphore的示例。
package com.hust.grid.leesf.semaphore;
import java.util.concurrent.Semaphore;
class MyThread extends Thread {
private Semaphore semaphore;
public MyThread(String name, Semaphore semaphore) {
super(name);
this.semaphore = semaphore;
}
public void run() {
int count = 3;
System.out.println(Thread.currentThread().getName() + " trying to acquire");
try {
semaphore.acquire(count);
System.out.println(Thread.currentThread().getName() + " acquire successfully");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(count);
System.out.println(Thread.currentThread().getName() + " release successfully");
}
}
}
public class SemaphoreDemo {
public final static int SEM_SIZE = 10;
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(SEM_SIZE);
MyThread t1 = new MyThread("t1", semaphore);
MyThread t2 = new MyThread("t2", semaphore);
t1.start();
t2.start();
int permits = 5;
System.out.println(Thread.currentThread().getName() + " trying to acquire");
try {
semaphore.acquire(permits);
System.out.println(Thread.currentThread().getName() + " acquire successfully");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println(Thread.currentThread().getName() + " release successfully");
}
}
}
运行结果(某一次):
main trying to acquire
main acquire successfully
t1 trying to acquire
t1 acquire successfully
t2 trying to acquire
t1 release successfully
main release successfully
t2 acquire successfully
t2 release successfully
说明:首先,生成一个信号量,信号量有10个许可,然后,main,t1,t2三个线程获取许可运行,根据结果,可能存在如下的一种时序。
说明:如上图所示,首先,main线程执行acquire操作,并且成功获得许可,之后t1线程执行acquire操作,成功获得许可,之后t2执行acquire操作,由于此时许可数量不够,t2线程将会阻塞,直到许可可用。之后t1线程释放许可,main线程释放许可,此时的许可数量可以满足t2线程的要求,所以,此时t2线程会成功获得许可运行,t2运行完成后释放许可。下面进行详细分析。
① main线程执行semaphore.acquire操作。主要的函数调用如下图所示。
说明:此时,可以看到只是AQS的state变为了5,main线程并没有被阻塞,可以继续运行。
② t1线程执行semaphore.acquire操作。主要的函数调用如下图所示。
说明:此时,可以看到只是AQS的state变为了2,t1线程并没有被阻塞,可以继续运行。
③ t2线程执行semaphore.acquire操作。主要的函数调用如下图所示。
说明:此时,t2线程获取许可不会成功,之后会导致其被禁止运行,值得注意的是,AQS的state还是为2。
④ t1执行semaphore.release操作。主要的函数调用如下图所示。
说明:此时,t2线程将会被unpark,并且AQS的state为5,t2获取cpu资源后可以继续运行。
⑤ main线程执行semaphore.release操作。主要的函数调用如下图所示。
说明:此时,t2线程还会被unpark,但是不会产生影响,此时,只要t2线程获得CPU资源就可以运行了。此时,AQS的state为10。
⑥ t2获取CPU资源,继续运行,此时t2需要恢复现场,回到parkAndCheckInterrupt函数中,也是在should继续运行。主要的函数调用如下图所示。
说明:此时,可以看到,Sync queue中只有一个结点,头结点与尾节点都指向该结点,在setHeadAndPropagate的函数中会设置头结点并且会unpark队列中的其他结点。
⑦ t2线程执行semaphore.release操作。主要的函数调用如下图所示。
说明:t2线程经过release后,此时信号量的许可又变为10个了,此时Sync queue中的结点还是没有变化。
五、总结
(1)Semaphore,也叫信号量,通常用于控制同一时刻对共享资源的访问上,也就是限流场景;
(2)Semaphore的内部实现是基于AQS的共享锁来实现的;
(3)Semaphore初始化的时候需要指定许可的次数,许可的次数是存储在state中;
(4)获取一个许可时,则state值减1;
(5)释放一个许可时,则state值加1;
(6)可以动态减少n个许可;
(7)可以动态增加n个许可吗?
彩蛋
(1)如何动态增加n个许可?
答:调用release(int permits)即可。我们知道释放许可的时候state的值会相应增加,再回头看看释放许可的源码,发现与ReentrantLock的释放锁还是有点区别的,Semaphore释放许可的时候并不会检查当前线程有没有获取过许可,所以可以调用释放许可的方法动态增加一些许可。
(2)如何实现限流?
答:限流,即在流量突然增大的时候,上层要能够限制住突然的大流量对下游服务的冲击,在分布式系统中限流一般做在网关层,当然在个别功能中也可以自己简单地来限流,比如秒杀场景,假如只有10个商品需要秒杀,那么,服务本身可以限制同时只进来100个请求,其它请求全部作废,这样服务的压力也不会太大。
使用Semaphore就可以直接针对这个功能来限流,以下是代码实现:
public class SemaphoreTest {
public static final Semaphore SEMAPHORE = new Semaphore(100);
public static final AtomicInteger failCount = new AtomicInteger(0);
public static final AtomicInteger successCount = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(()->seckill()).start();
}
}
public static boolean seckill() {
if (!SEMAPHORE.tryAcquire()) {
System.out.println("no permits, count="+failCount.incrementAndGet());
return false;
}
try {
// 处理业务逻辑【本篇文章由公众号“彤哥读源码”原创】
Thread.sleep(2000);
System.out.println("seckill success, count="+successCount.incrementAndGet());
} catch (InterruptedException e) {
// todo 处理异常
e.printStackTrace();
} finally {
SEMAPHORE.release();
}
return true;
}
}
作者:彤哥读源码
链接:www.spring4all.com/article/168…
来源:spring4all.com
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
作者:leesf
链接:www.cnblogs.com/leesf456/p/…
来源:cnblogs.com
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。