Redisson 的 AsyncSemaphore 源码剖析 聊聊 Semaphore 限流器
引子
前阵子看了看 redisson 的源码,也搜看了些网上的一些文章,发现讲主逻辑和流程的文章比较多,而对于一些细节的实现做出解释的文章比较少,其实我的上一篇文章也是如此 学了半天 synchronized 结果没用!还不快用分布式锁? 所以在此对 redisson 分布式锁中使用到的数据结构逐个做下解析,一起学习下他们的设计逻辑和思想。
源码包中 org.redisson.pubsub.PublishSubscribe#subscribe
PublishSubscribe.java
// 订阅 redis 锁释放事件
public CompletableFuture<E> subscribe(String entryName, String channelName) {
// 限流器,限流 50 个
AsyncSemaphore semaphore = service.getSemaphore(new ChannelName(channelName));
CompletableFuture<E> newPromise = new CompletableFuture<>();
semaphore.acquire(() -> {
// 省略非本期重点关注的实现………………
});
return newPromise;
}
大家都知道(大家是谁?) Redisson 在使用 RLock.lock() 方法加锁时,未抢到锁的线程会循环尝试获取锁,而如果几百几千个线程在这里无限自旋获取锁其实是非常消耗资源的,所以 Redisson 做了优化,对抢占的锁进行了锁释放事件的发布和订阅。这个时候未抢到锁的线程会订阅锁释放的事件。源代码如上。不知道的同学可以粗略过一眼 redisson 源码剖析 文章中加锁部分的讲解。
AsyncSemaphore semaphore = service.getSemaphore(new ChannelName(channelName));
方法的首行我们就看到了这样一个东西 AsyncSemaphore
可能不经常做多线程开发的同学并不熟悉 Semaphore
这个东西,所以本文我们主要从 Semaphore 开始聊起
Semaphore 信号量机制
Semaphore 普遍翻译叫做信号量,信号量机制。
在编程的领域里,线程能不能执行要看信号量允许不允许,其实这个概念也可以理解为信号灯,“绿灯行,红灯停” 。
和锁一样都是用于对某些稀缺资源的访问进行限制,而和锁不同的是 Semaphore 可以允许多个线程同时访问获取资源(具体几个取决于你的设置),所以锁可以看做是 Semaphore 的一个特殊实现。
先聊聊信号量模型吧
信号量模型
一般来说,一种机制的发明,都会有一个简易的,标准的模型提供给我们参考,信号量也是如此,令人高兴的是,他的模型并不复杂。
如上图所示,核心就是两个属性和三个方法。对应成类大概如下来理解:
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
public abstract class Semaphore {
/**
* 计数器
*/
private final AtomicInteger counter;
/**
* 等待队列
*/
private final Queue listeners = new ConcurrentLinkedQueue<>();
public Semaphore(int num) {
counter = new AtomicInteger(num);
}
/**
* counter 值减一后,如果大于等于 0 继续执行,否则阻塞
*/
public abstract void acquire();
/**
* counter 值加一后,如果小于等于 0 说明存在线程被阻塞,唤醒等待队列中的一个线程,并将该线程从等待队列中移除
*/
public abstract void release();
}
注意以上三个方法都是原子的。
李神在 java.util.concurrent 提供了 Semaphore 的实现 java.util.concurrent.Semaphore
Redisson 中的 AsyncSemaphore
本文重点看下 redisson 自己实现的一个 AsyncSemaphore ,异步限流器,主要是依赖了 juc 的 CompletableFuture 类,还蛮有意思的,需要注意的是这里 CompletableFuture 默认使用的是 ForkJoinPool 线程池。
这个线程池默认创建的线程数是 CPU 的核数(也可以通过 JVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelism 来设置 ForkJoinPool 线程池的线程数)一般来说我们在使用的时候要根据业务不同来创建不同的线程池给 CompletableFuture 来使用。
相关解释都写在代码注释中了,如下
package org.redisson.pubsub;
import java.util.Queue;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class AsyncSemaphore {
// 一个线程安全的计数器
private final AtomicInteger counter;
// 等待队列,泛型存了一个 CompletableFuture<Void>
private final Queue<CompletableFuture<Void>> listeners = new ConcurrentLinkedQueue<>();
// 相当于 init 方法,这里初始化计数器
public AsyncSemaphore(int permits) {
counter = new AtomicInteger(permits);
}
/**
* @Description: 有超时时间的 acquire。
* 这里的超时是限制了你传入的 CompletableFuture<Void> 执行的超时时间,而不是 acquire 的超时
* @Param timeoutMillis 超时时间/毫秒
* @Return boolean
*/
public boolean tryAcquire(long timeoutMillis) {
CompletableFuture<Void> f = acquire();
try {
f.get(timeoutMillis, TimeUnit.MILLISECONDS);
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} catch (ExecutionException e) {
throw new IllegalStateException(e);
} catch (TimeoutException e) {
return false;
}
}
/**
* @Description: 获取等待队列长度
* @Param
* @Return int
*/
public int queueSize() {
return listeners.size();
}
/**
* @Description: 清除等待队列
* @Param
* @Return void
*/
public void removeListeners() {
listeners.clear();
}
/**
* @Description: acquire 方法,返回 CompletableFuture
* @Param
* @Return java.util.concurrent.CompletableFuture<java.lang.Void>
*/
public CompletableFuture<Void> acquire() {
// 这里首先创建了一个 CompletableFuture 加入到了等待队列中
CompletableFuture<Void> future = new CompletableFuture<>();
listeners.add(future);
// 调用 tryRun() 方法,这里是限流的主要逻辑
tryRun();
return future;
}
/**
* @Description: acquire 方法的重载,区别是这里传一个 Runnable ,非阻塞直接执行,阻塞则等待后执行,是异步的
* 主要实现是 CompletableFuture 的 thenAccept 方法,这里是一个链式调用
* @Param listener
* @Return void
*/
public void acquire(Runnable listener) {
acquire().thenAccept(r -> listener.run());
}
/**
* @Description: 限流主逻辑,尝试运行,注意这里是一个通用方法
* @Param
* @Return void
*/
private void tryRun() {
while (true) {
// 先尝试 -1 如果计数器递减 >=0 则取队头的任务执行
if (counter.decrementAndGet() >= 0) {
// 去等待队列对头的任务
CompletableFuture<Void> future = listeners.poll();
// 等待队列没有任务,技术器 +1 直接返回
if (future == null) {
counter.incrementAndGet();
return;
}
// future.complete 将 future 置为完成,为后续 thenAccept 做铺垫
// complete 里面调用了 postComplete() 该方法就是通知任务执行完成。触发后续依赖任务执行
if (future.complete(null)) {
return;
}
}
// 尝试运行失败后 +1 ,计数器已经 <=0 则直接返回 (后续的 tryRun 会在 release 里尝试调用)
if (counter.incrementAndGet() <= 0) {
return;
}
}
}
// 获取计数器的值
public int getCounter() {
return counter.get();
}
// 释放资源 这里调用计数器 +1 然后 tryRun
public void release() {
counter.incrementAndGet();
tryRun();
}
@Override
public String toString() {
return "value:" + counter + ":queue:" + queueSize();
}
}
提一点,用完别忘了 release() ,否则不会触发后续任务的执行,毕竟逻辑都在 tryRun() 里了,最后给个使用示例
public static void main(String[] args) {
CompletableFuture<Void> future = new CompletableFuture<>();
System.out.println(future.complete(null));
System.out.println(future.complete(null));
future.thenAccept((r)->{
try {
Thread.sleep(2000);
System.out.println("dyinggq");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
AsyncSemaphore asyncSemaphore= new AsyncSemaphore(1);
asyncSemaphore.acquire(()->{
try {
Thread.sleep(2000);
System.out.println("dyinggq1");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
asyncSemaphore.release();
asyncSemaphore.acquire(()->{
try {
Thread.sleep(2000);
System.out.println("dyinggq2");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("the end");
}