Semaphore 信号量
-
作用:可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
举例:可以把它简单的理解成我们停车场入口立着的那个显示屏,每有一辆车进入停车场显示屏就会显示剩余车位减1,每有一辆车从停车场出去,显示屏上显示的剩余车辆就会加1,当显示屏上的剩余车位为0时,停车场入口的栏杆就不会再打开,车辆就无法进入停车场了,直到有一辆车从停车场出去为止。 -
内部原理
Semaphore内部主要通过AQS(抽象队列同步器:AbstractQueuedSynchronizer)实现线程的管理。Semaphore在构造时,需要传入许可证的数量,它最后传递给了AQS的state值。线程在调用acquire方法获取许可证时,如果Semaphore中许可证的数量大于0,许可证的数量就减1,线程继续运行,当线程运行结束调用release方法时释放许可证时,许可证的数量就加1。如果获取许可证时,Semaphore中许可证的数量为0,则获取失败,线程进入AQS的等待队列中,等待被其它释放许可证的线程唤醒。 -
应用场景: 主要用于那些资源有明确访问数量限制的场景,常用于限流
举例:
① 餐厅的叫号系统,餐厅用餐桌位是固定的,只有当顾客离开,才能进入用户用餐,否则只能在外排队等候。
② 数据库连接池,同时进行连接的线程有数量限制,连接不能超过一定的数量,当连接达到了限制数量后,后面的线程只能排队等前面的线程释放了数据库连接才能获得数据库连接。 -
公共方法
-
Semaphore 的两个构造函数
//在无参的构造函数中,使用的是NonfairSync(非公平锁),这个类不保证线程获得许可证的顺序,调用acquire方法的线程可以在一直等待的线程之前获得一个许可证。
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
//在调用有参的构造方法时,fair参数传入true,这样使用的是FairSync(公平锁),可以确保按照各个线程调用acquire方法的顺序获得许可证。
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
//当一个线程A调用acquire方法时,会直接尝试获取许可证,而不管同一时刻阻塞队列中是否有线程也在等待许可证,如果恰好有线程C调用release方法释放许可证,并唤醒阻塞队列中第一个等待的线程B,此时线程A和线程B是共同竞争可用许可证,不公平性就体现在:线程A没任何等待就和线程B一起竞争许可证了
final int nonfairTryAcquireShared(int acquires) {
//acquires参数默认为1,表示尝试获取1个许可证。
for (;;) {
int available = getState();
//remaining是剩余的许可数数量。
int remaining = available - acquires;
//剩余的许可数数量小于0时,
//当前线程进入AQS中的doAcquireSharedInterruptibly方法
//等待可用许可证并挂起,直到被唤醒。
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining;
}
}
//和非公平策略相比,FairSync中多一个对阻塞队列是否有等待的线程的检查,如果没有,就可以参与许可证的竞争;如果有,线程直接被插入到阻塞队列尾节点并挂起,等待被唤醒。
protected final boolean tryReleaseShared(int releases) {
//releases参数默认为1,表示尝试释放1个许可证。
for (;;) { int current = getState();
//next是如果许可证释放成功,可用许可证的数量。
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
//如果许可证释放成功, //当前线程进入到AQS的doReleaseShared方法,
//唤醒队列中等待许可的线程。
if (compareAndSetState(current, next))
return true;
}
}
acquire() :获取一个令牌,在获取到令牌、或者被其他线程调用中断之前线程一直处于阻塞状态。
acquire(int permits) :获取一个令牌,在获取到令牌、或者被其他线程调用中断、或超时之前线程一直处于阻塞状态。
acquireUninterruptibly():获取一个令牌,在获取到令牌之前线程一直处于阻塞状态(忽略中断)。
tryAcquire():尝试获得令牌,返回获取令牌成功或失败,不阻塞线程。
tryAcquire(long timeout, TimeUnit unit) :尝试获得令牌,在超时时间内循环尝试获取,直到尝试获取成功或超时返回,不阻塞线程。
release() :释放一个令牌,唤醒一个获取令牌不成功的阻塞线程。
hasQueuedThreads() :等待队列里是否还存在等待线程。
getQueueLength():获取等待队列里阻塞的线程数。
drainPermits():清空令牌把可用令牌数置为0,返回清空令牌的数量。
availablePermits():返回可用的令牌数量。
- 测试类
package com.wujian.love.study.thread;
import java.util.Random;
import java.util.concurrent.Semaphore;
/**
* @ClassName: SemaphoreTest
* @Description: 信号量代码测试
* @Author: wuj
* @Date: 2021-03-08 17:00
**/
public class SemaphoreTest {
//饭店里面有10个座位
private static Semaphore semaphore=new Semaphore(2,true);
public static void main(String[] args) {
//模拟100个人来吃饭
for(int i=0;i<5;i++){
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("===="+Thread.currentThread().getName()+"来到饭店");
//返回可用的令牌数量
int seat = semaphore.availablePermits();
if(seat ==0 ){
System.out.println("座位不足,请耐心等待");
}
//获取令牌尝试进入饭店
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"成功进入饭店");
//模拟吃饭时间
Thread.sleep(new Random().nextInt(10000));
System.out.println(Thread.currentThread().getName()+"离开饭店");
//释放令牌,腾出座位
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},i+"号客人");
thread.start();
}
}
}
- 控制台打印结果
====1号客人来到饭店
====3号客人来到饭店
====2号客人来到饭店
====0号客人来到饭店
====4号客人来到饭店
座位不足,请耐心等待
座位不足,请耐心等待
座位不足,请耐心等待
3号客人成功进入饭店
1号客人成功进入饭店
1号客人离开饭店
4号客人成功进入饭店
4号客人离开饭店
0号客人成功进入饭店
3号客人离开饭店
2号客人成功进入饭店
0号客人离开饭店
2号客人离开饭店