我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。
在前两篇中,我们分别介绍了AQS的独占和共享两种模式的源码分析以及各自的典型应用实现,重入锁ReentrantLock和CountDownLatch。点击下方链接即可阅读,希望按照顺序哈~
第一篇:Java多线程第九篇--ReentrantLock与AbstractQueuedSynchronizer的恩怨情仇
第二篇:Java多线程第十篇--通过CountDownLatch再探AbstractQueuedSynchronizer的共享模式
今天我们将介绍另一个利器Semaphore,这个工具类应该也是J.U.C包里,在平时工作中接触的较多的一个类了。老样子,我们将从简单介绍、使用场景、用法、工作原理(源码分析)几个方面来对Semaphore进行全面的剖析。
(PS:但还是老话,希望在看此篇之前,能够先去看下前面两篇,如果你懂了那就没必要了)
简单介绍
Semaphore到底是用来干嘛的,它有哪些应用场景,网上其实有很多的文章都做过介绍,但我看了很多都没有说的很全面(PS:当然我可能也没有说的很全面哈~)
要说最全的说法,其实仔细看Semaphore的源码应该都会知道,在Semaphore的类上面的注释,已经把Semaphore是什么?可以怎么用?用于哪些场景已经说的很清晰了。
定义
首先,从它的定义来说(其实也就是类的英文注释):Semaphore的意思是信号量,它维护了一组许可证,多个线程可以通过这个类做到资源以及线程间的协同(即他和我们前面说的CountDownLatch一样,也是线程间进行协作或者同步的一个工具类)。
线程可以通过acquire方法在必要时进行阻塞,直到拿到许可证,然后做对应的事情;也可以通过release方法,添加一个许可证(其实是释放回归),并且在有些情况下,还可以释放一个正在阻塞状态的线程。
使用场景
- 场景1:Semaphore通常用来限制一定数量的线程去访问某些资源,也就是说它可以用来在访问一些资源的时候,控制能使用这些资源的线程数量。这也是最常用的场景。
- 比如我们最常用的数据库连接数;再比如微服务当中限流降级中间件(hystrix、sentinel等)。使用Semaphore可以帮助我们有效管理这些有限资源的使用。
- 场景2:将信号量的资源值初始化为1,并且在使用过程中只能为1,这样就可以模拟互斥锁(独占锁)的功能,但是这个互斥锁和我们之前学习过的ReentrantLock有区别,因为ReentrantLock只能是谁加锁,谁解锁,而由于Semaphore实现的特殊性,它可以由其他线程释放锁。
- 这个功能可以在一些特殊的场景中应用,比如死锁的恢复。
- 场景3:场景2中将资源值设为1,可以模拟独占锁,当然,设置为大于1的值,就可以模拟共享锁,且还可以实现公平和非公平的共享锁。(按照前面文章的分析,很显然,非公平的吞吐量大于公平)
- 比如银行窗口排队的场景,通常几个窗口可以处理的业务的是一样的,就相当于几个资源,但同一时间只能同时有窗口数量的客户办理业务,其他人则在大厅等候,等待窗口没人,资源释放掉,才可以对等候的客户进行唤醒。
- 场景4:此类在获取和释放锁资源的个数上,除了通常获取和释放一个资源之外,还支持一次性获取和释放多个资源。
使用注意事项
- 使用非公平的锁时,会出现永远拿不到锁的情况,这个其实和ReentrantLock的公平实现是一样的。
- 在调用Semaphore的常用API时,往往一个线程的release发生于另一个线程的acquire(遵循Java内存模型的happen-before原则)
以上内容呢,其实都是类的注释,大家可以仔细阅读源码。
用法(上DEMO示例)
用法1:实现简易的资源链接池
public class SemaphoreResourcePool {
//可用资源大小
private static final int MAX_POOL_SIZE = 50;
//资源池信号量,控制线程数
private final Semaphore available = new Semaphore(MAX_POOL_SIZE, true);
//可用资源列表
protected Object[] resources = new Object[MAX_POOL_SIZE];
//记录资源列表的可用状态
protected boolean[] used = new boolean[MAX_POOL_SIZE];
//获取资源,获取之前,调用acquire方法,看他能够取到资源,可以的话,执行getNextAvailableItem
//不行的话,阻塞等待
public Object getResource() throws InterruptedException {
available.acquire();
return getNextAvailableItem();
}
//释放回归资源进池子
public void putItem(Object x) {
if (markAsUnused(x))
available.release();
}
//取可用资源,并标记资源被占用了
protected synchronized Object getNextAvailableItem() {
for (int i = 0; i < MAX_POOL_SIZE; ++i) {
if (!used[i]) {
used[i] = true;
return resources[i];
}
}
return null; // not reached
}
//标记资源可用了
protected synchronized boolean markAsUnused(Object item) {
for (int i = 0; i < MAX_POOL_SIZE; ++i) {
if (item == resources[i]) {
if (used[i]) {
used[i] = false;
return true;
} else
return false;
}
}
return false;
}
}
用法2:实现共享锁
场景描述:某银行提供了办理个人业务的窗口,总共设立了3个窗口(这3个窗口假设都办理同类型的业务,即它们是相同的资源),即同一时间最多只能有3个客户在办理业务,当有人办理完成,则窗口业务人员就会进行叫号,这时就会有客户到窗口办理业务。(这里假设客户办理业务是讲究先来后到,一般银行都会让你先取号)
public class BankDemo {
// 窗口资源数,公平模式,场景需要先来后到
private static Semaphore windows = new Semaphore(3, true);
public static void main(String[] args) {
// 假设银行一下子来了8个客户
for (int i = 0; i < 8; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 先问下有没有窗口提供服务,没有则等待,有的话 ,就处理业务
try {
System.out.println(Thread.currentThread().getName() + "问还有没有窗口");
windows.acquire();
System.out.println(Thread.currentThread().getName() + ",有窗口了,办理业务中....");
// 模拟客户办理业务
Random random = new Random();
int time = (random.nextInt(3) + 1);
for (int i = 0; i < time; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "办理业务中..." + i);
}
System.out.println(Thread.currentThread().getName() + "办理业务结束,窗口业务人员叫号....");
System.out.println("当前正在办理业务的人数:" + (3 - windows.availablePermits()) + " 当前排队人数:"
+ windows.getQueueLength());
callNextCustomer();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "thread-" + (i + 1));
thread.start();
}
}
public static void callNextCustomer() {
windows.release();
}
}
运行效果:
thread-2问还有没有窗口
thread-8问还有没有窗口
thread-8,有窗口了,办理业务中....
thread-7问还有没有窗口
thread-7,有窗口了,办理业务中....
thread-3问还有没有窗口
thread-4问还有没有窗口
thread-5问还有没有窗口
thread-1问还有没有窗口
thread-6问还有没有窗口
thread-2,有窗口了,办理业务中....
thread-7办理业务中...0
thread-2办理业务中...0
thread-8办理业务中...0
thread-8办理业务结束,窗口业务人员叫号....
当前正在办理业务的人数:3 当前排队人数:5
thread-3,有窗口了,办理业务中....
thread-7办理业务中...1
thread-7办理业务结束,窗口业务人员叫号....
当前正在办理业务的人数:3 当前排队人数:4
thread-2办理业务中...1
thread-2办理业务结束,窗口业务人员叫号....
当前正在办理业务的人数:3 当前排队人数:3
thread-5,有窗口了,办理业务中....
thread-3办理业务中...0
thread-3办理业务结束,窗口业务人员叫号....
当前正在办理业务的人数:3 当前排队人数:2
thread-4,有窗口了,办理业务中....
thread-1,有窗口了,办理业务中....
thread-5办理业务中...0
thread-5办理业务结束,窗口业务人员叫号....
当前正在办理业务的人数:3 当前排队人数:1
thread-4办理业务中...0
thread-6,有窗口了,办理业务中....
thread-1办理业务中...0
thread-1办理业务中...1
thread-1办理业务结束,窗口业务人员叫号....
当前正在办理业务的人数:3 当前排队人数:0
thread-4办理业务中...1
thread-4办理业务结束,窗口业务人员叫号....
thread-6办理业务中...0
当前正在办理业务的人数:2 当前排队人数:0
thread-6办理业务结束,窗口业务人员叫号....
当前正在办理业务的人数:1 当前排队人数:0
目前就列举这两个比较典型的例子吧,当然小伙伴们也可以自己想一个场景,来动手使用一下,我们代码人切忌眼高手低哈~
源码分析
通过查阅源码得知,其实Semaphore的实现方式和之前我们一起学习的ReentrantLock、CountDownLatch是一样的,在其内部自定义实现了一个同步器Sync,然后又派生了两个实现类,一个是公平实现,一个是非公平。然后在Semaphore的内部维护了一个同步器,用来实现具体的API。如下图:
获取许可证acquire系列的源码分析
acquire系列一共有四个方法,其本质调用如下:
- acquire() ----> sync.acquireSharedInterruptibly(1)
- acquireUninterruptibly()---->sync.acquireShared(1)
- acquire(int permits)---->sync.acquireSharedInterruptibly(permits)
- acquireUninterruptibly(int permits) ----> sync.acquireShared(permits) 由上面的可以知道,其实最终调用的就是两个方法,sync.acquireSharedInterruptibly(permits)和sync.acquireShared(permits)一个是是响应中断,还有个是不响应中断的区别而已。至于里面关于AQS部分的实现,在前面的两篇文章已经详细分析过了。下面我们主要来看自定义同步器的实现过程:
直接以acquire()方法的调用路径分析吧: acquire()->sync.acquireSharedInterruptibly(1)->tryAcquireShared(1)
//sync.acquireSharedInterruptibly(1)
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
下面我们先来回忆下tryAcquireShared方法的定义:
- 返回值小于0:获取锁资源失败
- 返回值等于0:获取锁资源成功,且再有线程来获取,将会不成功
- 返回值大于0:获取锁资源成功,再有线程来获取,有可能会成功 那么在Semaphore的解释就是,tryAcquireShared的返回值就是信号量的资源个数了,小于0的意思就代表资源不够了。
在前面文章的基础上,我们很容易知道tryAcquireShared就是留给自定义同步器实现的,我们跟踪代码,发现:最终的实现有两种,一个是公平,一个是非公平。如下代码:
//公平实现
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;
}
}
//非公平实现
protected int tryAcquireShared(int acquires) {
//其实调用的是父类的实现
return nonfairTryAcquireShared(acquires);
}
//nonfairTryAcquireShared(acquires)
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
- 在这两种实现中,其实区别已经很明显了,公平锁有个函数的判断,hasQueuedPredecessors()函数我们之前分析过,就是用来判断当前线程是否需要排队的,在公平模式里,如果需要排队,则直接返回-1,代表获取资源直接失败,下一步就是排队等待(为什么直接返回-1不用解释了吧。原因就是“需要排队”。。)。
- 除了上面的排队判断,我们再来看方法体的具体实现,我们发现简直就是一模一样,都是同样的for循环自旋操作,直到remaining < 0 || compareAndSetState(available, remaining)成立,就会退出循环。
- remaining小于0,代表的意思就是资源数不够减,资源数不够,那肯定就是失败,所以直接返回退出循环;
- 如果资源数够即remaining大于等于0,而且CAS修改成功,则也返回退出循环;
- 否则,继续自旋,继续尝试CAS操作获取资源。
- 这里之所以是for自旋,是因为当前时间前来获取许可证的有很多线程,并发!!,所以我们必须用for自旋+CAS的方式保证当前时间只有一个线程修改成功。
当tryAcquireShared返回值大于等于0,则代表成功获得许可证;如果小于0,则失败,将进入 doAcquireSharedInterruptibly(arg)方法,这个方法就不叙述了,看这里。
以上便是acquire的流程了,是不是轻松加愉快,当然这是建立在前面两篇文章的基础之上的。
释放回归资源release系列的源码分析
这个系列就简单多了,一共的API就两个:
- release() ----> sync.releaseShared(1)
- release(int permits)----> sync.releaseShared(permits) 说是两个,只是不同的应用场景,但从源码角度,实质上就一个方法 sync.releaseShared(permits)
//AQS.releaseShared
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
这个方法,首先进入tryReleaseShared(arg),关于这个方法的定义之前的文章已经分析过,不再叙述~
我们直接看tryReleaseShared(arg)的自定义同步器的方法实现,这个方法在这里就没公平和非公平的区别了,他们都调用的父类Sync的tryReleaseShared方法:
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;
}
}
这段代码就很简单了,for循环自旋+CAS将资源值进行归还。退出自旋的条件就是归还资源值成功。 至于后续的具体释放资源的过程,见这里。
tryAcquire系列
我们在上面说过,注意下,这边的tryAcquire并非我们之前讲独占锁的AQS的方法,这个方法在Semaphore直接实现的,所以请不要混肴了。(其实一开始我也有点晕,直到我看了方法的注释之后,我才知道方法的作用)
/**
* Acquires a permit from this semaphore, only if one is available at the
* time of invocation.
*
* <p>Acquires a permit, if one is available and returns immediately,
* with the value {@code true},
* reducing the number of available permits by one.
*
* <p>If no permit is available then this method will return
* immediately with the value {@code false}.
*
* <p>Even when this semaphore has been set to use a
* fair ordering policy, a call to {@code tryAcquire()} <em>will</em>
* immediately acquire a permit if one is available, whether or not
* other threads are currently waiting.
* This "barging" behavior can be useful in certain
* circumstances, even though it breaks fairness. If you want to honor
* the fairness setting, then use
* {@link #tryAcquire(long, TimeUnit) tryAcquire(0, TimeUnit.SECONDS) }
* which is almost equivalent (it also detects interruption).
*
* @return {@code true} if a permit was acquired and {@code false}
* otherwise
大体翻译:
如果有许可证,则获取许可证,并立即返回,返回值为true,将可用许可证的数量减少1。
如果没有可用的许可证,该方法将立即返回值为false。
即使这个信号量被设置为使用公平排序策略,如果有一个可用的许可,对tryAcquire()的调用将立即获得许可,
无论当前是否有其他线程在等待。这种“barging”行为在某些情况下是有用的,尽管它破坏了公平性。如果您想
遵守公平设置,那么使用tryAcquire(0, TimeUnit.SECONDS),它几乎是等价的(它还检测中断)
*/
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
下面我们通过比较acquire和tryAcquire的形式,来分析下tryAcquire到底可以干嘛
//acquire
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
//tryAcquire
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
从调用的路径来看,区别如下:
acquire先调用tryAcquireShared(arg),tryAcquireShared(arg)的实现有两种模式:公平和非公平;而tryAcquire直接调用的sync.nonfairTryAcquireShared(1),这个我们上面分析了,其实是非公平的实现。再结合上面的翻译:意思就是很明显了,tryAcquire()方法可以使得线程立即获取到许可证(如果有的话),即使当前是公平模式,它可以打破公平,只要有资源,就可以成功抢占。
此方法一般可以用作特殊情况下的处理,比如有个高优先级的线程来了,比如上面的银行例子中,来了个“VIP中P”客户;或者我们在debug调试的时候,我们想制造一个资源被其他线程占用的场景等等。 至于其他的重载方法的实现,我们就不一一叙述了,小伙伴们按照上面的思路自行验证哈~
其他API的分析
public int availablePermits()
// 获取可用许可证的数量,可以用作统计,判断等用途,看你自己哈
public int availablePermits() {
return sync.getPermits();
}
public int drainPermits()
/**
* Acquires and returns all permits that are immediately available.
*
* @return the number of permits acquired
*/
public int drainPermits() {
return sync.drainPermits();
}
//Sync.drainPermits
final int drainPermits() {
for (;;) {
int current = getState();
if (current == 0 || compareAndSetState(current, 0))
return current;
}
}
源码的实现很简单:就是一下子将所有的资源都消耗光,让线程无资源可用,并且返回消费了多少资源。这个很霸道的操作目前还不知道适合什么场景使用。。。DEBUG?留给小伙伴们思考~
protected void reducePermits(int reduction)
/**
* Shrinks the number of available permits by the indicated
* reduction. This method can be useful in subclasses that use
* semaphores to track resources that become unavailable. This
* method differs from {@code acquire} in that it does not block
* waiting for permits to become available.
*
* @param reduction the number of permits to remove
* @throws IllegalArgumentException if {@code reduction} is negative
*/
protected void reducePermits(int reduction) {
if (reduction < 0) throw new IllegalArgumentException();
sync.reducePermits(reduction);
}
//Sync.reducePermits
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;
}
}
结合注释和源码,这个函数大概意思如下:可以用来存粹的减少许可证的数量,与acquire不同的是,这个方法只是减少资源,不管获取结果如何,都不会阻塞线程(有可能会将资源数量搞成负数)。此外,该方法的修饰符是protected,即它可以被子类重写,即可扩展。至于使用场景,其实注释里也说了可用于在子类中跟踪不可用资源;还有就是既然它可以减少资源,且不会阻塞线程,是不是意味着可以动态的改变资源池子大小。。。更何况它还可以扩展。。。
总结
以上便是Semaphore的全部分析了,由上面的种种分析来看,总结如下:
- Semaphore可以是一个有效的限流器,可以用来对有限资源的使用控制;
- Semaphore也可以是互斥锁/共享锁,但是和之前的锁不同的是,加锁和释放锁可以不是同一个线程;
- Semaphore是AQS共享模式的一个应用实现类,它可以实现公平和非公平,公平可以实现先后次序,但吞吐量没有非公平高;非公平也有可能造成线程的“饿死”;
- Semaphore还有一系列的工具方法和扩展方法,可以帮助我们实现一些扩展业务或者更变态的业务需求,也可以帮助我们在开发过程中进行代码的测试和验证。
到此,AQS类的第三个应用实现工具类Semaphore的分析到此结束,其实前两篇的ReentrantLock和CountDownLatch也是AQS类的应用实现类。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。