Semaphore信号量
Semaphore信号量
什么是信号量
信号量由著名计算机科学家迪杰斯特拉(Dijkstra)于 1965 年提出,提出后的15年对并发编程领域是统治地位,直到1980年管程的提出才有了第二选择。
信号量以前也称为红绿灯,车能不能过取决于信号灯让不让过,并发编程也是,线程能不能执行取决信号量让不让放行。
信号量模型
信号量模型主要分为一个计数器、一个等待队列、三个方法,信号量模型规定只能通过三个方法来访问计算器和等待队列,模型如下所示。
对应三个方法有如下解释:
init方法:信号量计算器初始化方法。
down方法:count值减一操作,如果此时count值小于0,那么线程被阻塞,进入等待队列中。
up方法:count值加一操作,如果此时count值小于等于0,那么从等待队列中唤醒一个线程,同时等待队列移除该线程。
需要注意的是这三个方法都能够保证其原子性,并且这个原子性是由信号量的实现方保证,也就是我们提到的java.util.concurrent.Semaphore。
如何使用信号量
信号量的具体实现Semaphore如何使用?我们可以从其本意的现实意义出发红绿灯,每次车辆通过都要查看是否是绿灯,只有绿灯才通行,这个是不是和锁的加锁规则有点相似呢。确实如此如果将count的数量设置为1,只允许一个线程通行那这就是互斥锁的具体实现,代码逻辑如下。
public class Test {
public static void main(String[] args) {
TestSemaphore testSemaphore = new TestSemaphore();
for (int i = 0; i <100 ; i++) {
new Thread(()->{
testSemaphore.addOne();
},"test".concat(String.valueOf(i))).start();
}
}
}
class TestSemaphore{
private static int count=0;
static final Semaphore semaphore = new Semaphore(1);
public static void addOne(){
try {
semaphore.acquire();
count = count + 1;
System.out.println(Thread.currentThread().getName()+"==="+count);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
}
}
信号量如何实现互斥
当两个线程T1,T2同时执行addOne方法,
T1执行到semaphore.acquire();
形如信号量模型的down方法,注意这是一个原子操作,所以只能有一个线程访问假设T1进入,将计数器减一,这时计数器变为0,另外一个线程T2则将计数器减一变为-1,根据down方法规则小于0需要被阻塞所以T2线程进入等待队列中。
T1执行count = count + 1;
count的值变为1,最后线程T1执行semaphore.release();
形如信号量的up方法,将计数器加一,这时计数器从-1变为0,根据up方法规则需要从等待队列唤醒一个线程,也就是T2,并且将T2从等待队列中移除,T2继续往下执行,从而实现了互斥操作。
信号量的其它使用场景
那么信号量是否有其它的使用场景呢,在上诉代码中演示了如何实现互斥锁,如果信号量只有这一个功能那么SDK为什么不直接用Lock来做呢?当然这不是它的主要功能,信号量能够允许多个线程访问同一个临界区。
如果将计数器的值设置为N,那么信号量允许N个线程访问同一个临界区,这种需求场景比较常见的就是各种池化资源,例如线程池,数据库连接池、对象池等,在同一个时刻允许N个线程同时访问连接池,当然每个连接在未被释放之前都是不允许其它线程使用的。
说到各种池化资源这里都有的一个共同特点就是只允许同时有连接数N个的线程访问,超过线程池个数N的线程只能等待,那这不就是限流器的简单实现吗,所以信号量能实现简单的限流器。
实现简单的限流器
/**
* 用信号量模型Semaphore实现限流器
* @param <T>
* @param <R>
*/
public class SemaphoreObjPool<T,R> {
// 池化资源容器
private List<T> pool;
// 信号量
private Semaphore semaphore = null;
public SemaphoreObjPool(int size,T t){
// 不要选用ArrayList多线程访问同一个资源池时需要注意线程安全问题
pool = new Vector<T>(){};
for (int i = 0; i <size ; i++) {
pool.add(t);
}
// 初始化计数器
semaphore = new Semaphore(size);
System.out.println("资源池初始化资源:"+pool);
}
R exec(Function<T,R> function){
T remove = null;
try {
semaphore.acquire();
// 从资源池获取连接
remove = pool.remove(0);
return function.apply(remove);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 连接用完放入资源池
pool.add(remove);
semaphore.release();
}
return null;
}
public static void main(String[] args) {
SemaphoreObjPool semaphoreObjPool = new SemaphoreObjPool(10,2L);
new Thread(()->{
semaphoreObjPool.exec(t->{
System.out.println(Thread.currentThread().getName()+"=="+t);
return t;
});
}).start();
}
}
这里简单的使用Semaphore实现了限流器,其主要方法是exec,每个线程经过semaphore.acquire();
时计数器都会减一,一旦当计数器的值小于0那么阻塞线程进入等待队列等待,直到有线程执行semaphore.release();
方法后发现计数器依然小于或等于0那么等待队列中肯定存在等待线程,直接唤醒等待队列中的一个线程去从资源池获取连接,这样限流器的目的就达到了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~