使用Semaphore 实现一个简单的限流器
java api
Java的api中,提供了semaphore这个线程同步的辅助类,用来控制同时访问共享资源的线程数量。
Semaphore提供的主要方法如下:
void acquire():获取一个信号量,在获得信号量前线程会一直阻塞。
void release():释放一个信号量。
int availablePermits(): 返回当前可用的信号量数。
boolean hasQueuedThreads(): 查询是否有等待获取信号量的线程。
实际上,semaphore实现的是并发编程领域的信号量模型。
信号量模型
信号量模型,是Dijkstra在1965年提出的理论,也是长时间并发编程领域的经典理论。现在并发编程领域主流用的比较多的是另外一个理论,就是管程,后面会单独介绍管程。
信号量模型,主要是由计数器,等待队列组成。
首先,要先初始化信号量模型,就是设置计数器的初始值。
然后,线程A来获取信号量,如果计数器中有信号量,那么线程得到信号量,同时将计数器减1
然后,线程A执行完,就释放之前得到的信号量,同时将计数器加1
如果在线程A获取得到信号量的执行过程中,有另外一个线程B来申请获取信号量,此时要看计数器是否还有信号量剩余,如果有,那么这另外的一个线程B就能够得到信号量执行,如果没有,这另外的线程B就要阻塞,进入等待队列中。
当线程A执行完,归还信号量,那么就会去唤醒等待队列中的信号量。
信号量计数器加减的过程,实际上也就是操作系统领域的PV操作,也有的叫PV原语
PV操作
操作系统中,可以把线程简单的抽象成三个状态,线程的执行过程,实际上就是这三态在某些条件下的转换
三态转换的过程就是PV操作来执行的,
P操作执行,获取到资源,信号量减1
V操作执行,释放资源,信号量加1
这里,就不深入的写操作系统相关的基础知识了。
信号量和锁
如果将信号量的计数器大小设置为1,那么这个信号量实际上跟java lock差不多,就是一个简单的锁的实现。
但是,如果将信号量的计数器大小设置大于1,那么,这点是java lock 是做不到的,锁只能保障只有一个线程能够获得临界资源,但是信号量可以支持多个线程同时获得临界资源,这也就促成了信号量可以实现一个简单版本的限流器
代码实现
接下来,就是写一个简单的代码实现,首先建立一个线程池,线程池的大小是10,再新建一个semaphore变量,将大小设定为5。
大概的逻辑,首先提交任务到线程池,然后线程池执行任务,在执行任务的过程中,受semaphore信号量的控制,最多并发执行只有5个任务。
import java.util.concurrent.*;
public class SemaphoreLimitTest {
// 定义一个执行线程池
private final Executor executor = new ThreadPoolExecutor(10, 20, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(10));
// 每次只能执行5个任务
private final Semaphore semaphore = new Semaphore(5);
private void process() {
executor.execute(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
Thread.sleep(3000);
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
// 模拟测试
public static void main(String[] args) {
final SemaphoreLimitTest semaphoreLimitTest = new SemaphoreLimitTest();
// 同时进来8个任务
for (int i = 0; i < 8; i++) {
// 定义8个线程
new Thread("线程" + i) {
@Override
public void run() {
semaphoreLimitTest.process();
}
}.start();
}
}
}
semaphore的不足
信号量在释放后,要去唤醒等待队列中的阻塞线程,这个地方的实现是只能唤醒一个阻塞中的线程,而不能同时唤醒多个线程去争抢临界资源。
另外一个问题是,只能唤醒一个阻塞中的线程,但是线程被唤醒后,可能会出现临界条件又不满足了,那么这个线程又会进入阻塞