持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第18天,点击查看活动详情
一、Semaphore介绍
Semaphore,俗称信号量,它是操作系统中PV操作的原语在java的实现,它也是基于AbstractQueuedSynchronizer实现的。
Semaphore的功能非常强大,大小为1的信号量就类似于互斥锁,通过同时只能有一个线程获取信号量实现。大小为n(n>0)的信号量可以实现限流的功能,它可以实现只能有n个线程同时获取信号量。
PV操作是操作系统一种实现进程互斥与同步的有效方法。PV操作与信号量(S)的处理相关,P表示通过的意思,V表示释放的意思。用PV操作来管理共享资源时,首先要确保PV操作自身执行的正确性。
P操作的主要动作是:
①S减1;
②若S减1后仍大于或等于0,则进程继续执行;
③若S减1后小于0,则该进程被阻塞后放入等待该信号量的等待队列中,然后转进程调度。
V操作的主要动作是:
①S加1;
②若相加后结果大于0,则进程继续执行;
③若相加后结果小于或等于0,则从该信号的等待队列中释放一个等待进程,然后再返回原进程继续执行或转进程调度。
二、Semaphore 常用方法
2-1、构造器
- permits 表示许可证的数量(资源数)
- fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程
2-2、常用方法
public void acquire() throws InterruptedException
public boolean tryAcquire()
public void release()
public int availablePermits()
public final int getQueueLength()
public final boolean hasQueuedThreads()
protected void reducePermits(int reduction)
各方法作用如下,标红为常用方法
acquire() 表示阻塞并获取许可tryAcquire() 方法在没有许可的情况下会立即返回 false,要获取许可的线程不会阻塞release() 表示释放许可- int availablePermits():返回此信号量中当前可用的许可证数。
- int getQueueLength():返回正在等待获取许可证的线程数。
- boolean hasQueuedThreads():是否有线程正在等待获取许可证。
- void reducePermit(int reduction):减少 reduction 个许可证
- Collection getQueuedThreads():返回所有等待获取许可证的线程集合
三、应用场景
可以用于做流量控制,特别是公用资源有限的应用场景
3-1、模拟买票场景
在互联网还没席卷的时候,我们购买火车票都需要去火车站排队购买票,买票的人很多,可是售票窗口是有限的,下面用Semaphore来模拟实现一个买票的场景
如下使用Semaphore来模拟售票窗口,设置的资源数为3,使用for循环来模拟排队购买的人员,当for执行,进入run方法,使用windows.acquire()加锁的时候,实际上就是占用了一个资源,第二第三再次进入的时候,也同时占用一个资源,当第四个再进入的时候,因为已经没有资源,就会加入的排队的状态。当前面三个其中一个人释放锁的时候,后面排队的才会加锁成功。
需要注意,使用的时候一定要在finally中使用release解锁,防止程序异常无法释放锁
public class SemaphoreTest {
public static void main(String[] args) {
// 声明3个窗口 state: 资源数
Semaphore windows = new Semaphore(3);
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
// 占用窗口 加锁
windows.acquire();
System.out.println(Thread.currentThread().getName() + ": 开始买票");
//模拟买票流程
Thread.sleep(5000);
System.out.println(System.currentTimeMillis());
System.out.println(Thread.currentThread().getName() + ": 购票成功");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放窗口
windows.release();
}
}
}).start();
}
}
}
执行结果如下,可以看到线程1/2/4抢占到了锁,后面的线程就会进入排队等待的状态。
3-2、限流
如下示例,定义了一个5个请求的限流器,然后创建一个线程池,在main方法中使用for实现了一个无线循环,利用线程池执行方法,由于方法中使用了限流器,这样每次就只允许5个线程执行,其他线程进行排队,当前5个线程执行完成释放锁之后,后面的线程再抢占锁,这样虽然需要等待的线程有很多,但是通过使用Semaphone实现了一个限流器,就每次只允许5个任务同时执行
public class SemaphoneTest2 {
/**
* 实现一个同时只能处理5个请求的限流器
*/
private static Semaphore semaphore = new Semaphore(5);
/**
* 定义一个线程池
*/
private static ThreadPoolExecutor executor = new ThreadPoolExecutor
(10, 50, 60,
TimeUnit.SECONDS, new LinkedBlockingDeque<>(200));
/**
* 模拟执行方法
*/
public static void exec() {
try {
//占用1个资源
semaphore.acquire(1);
//TODO 模拟业务执行
System.out.println("执行exec方法");
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放一个资源
semaphore.release(1);
}
}
public static void main(String[] args) throws InterruptedException {
{
for (; ; ) {
Thread.sleep(100);
// 模拟请求以10个/s的速度
executor.execute(() -> exec());
}
}
}
}
执行结果: