引言
我们今天一起来了解一下juc的同步工具类-semaphore的基本用法。
什么是semaphore(信号量)
semaphore (信号量) 是 java.util.concurrent 包下非常有用的一个并发工具类。你可以把它理解为用于控制同时访问特定资源的线程数量的工具。它维护了一组“许可”(permits),线程在访问受保护资源前必须先获取一个许可,使用完后再释放该许可。
场景引入
想象一个只有 5 个车位的停车场(共享资源):
- permits (许可证/令牌): 初始化时,semaphore 拥有 5 个令牌。
- acquire (获取): 车辆(线程)进入时,先领 1 个令牌。如果没有令牌了(计数为0),车辆就得在门口排队(阻塞)。
- release (释放): 车辆离开时,归还 1 个令牌。此时如果有排队的车辆,就会唤醒其中一个进入。
这就是semaphore做的事,控制在一个时间段内可以访问特定资源的线程数量。
基本概念
- 许可(permit) :semaphore 内部维护一个计数器,表示当前可用的许可数量。
- acquire() :尝试获取一个或多个许可。如果没有足够许可,线程会被阻塞,直到有足够许可可用。
- release() :释放一个或多个许可,增加可用许可数量,可能唤醒等待中的线程。
底层原理
semaphore 内部基于 aqs (abstractqueuedsynchronizer) 实现。
- 它使用 aqs 的
state变量来存储当前的许可证数量。 acquire()操作是对state进行 cas 减法。release()操作是对state进行 cas 加法。
常用方法
下面是semaphore的常用方法。
| 方法 | 描述 | 场景 |
|---|---|---|
| new semaphore(int permits) | 创建非公平信号量(默认)。 | 吞吐量优先 |
| new semaphore(int permits, boolean fair) | 创建公平信号量(fifo)。 | 避免线程饥饿 |
| acquire() | 获取1个许可,若无则阻塞。响应中断。 | 必须拿到的场景 |
| acquire(int n) | 一次获取 n 个许可。 | 批量资源 |
| tryacquire() | 尝试获取,成功返回 true,失败返回 false,不阻塞。 | 快速失败/降级处理 |
| tryacquire(long timeout, timeunit unit) | 带超时的尝试获取。 | 避免长时间死等 |
| release() | 释放1个许可。 | 务必在 finally 中调用 |
| void release(int permits) | 释放指定数量许可。 | 务必在 finally 中调用 |
使用例子
这是最典型的使用场景:限流 (rate limiting) 或 资源池控制。
import java.util.concurrent.executorservice;
import java.util.concurrent.executors;
import java.util.concurrent.semaphore;
import java.util.concurrent.timeunit;
public class semaphoreexample {
// 定义一个只能允许3个线程同时访问的信号量
private static final semaphore semaphore = new semaphore(3);
public static void main(string[] args) {
executorservice executor = executors.newfixedthreadpool(10);
// 模拟10个请求同时涌入
for (int i = 0; i < 10; i++) {
final int threadnum = i;
executor.execute(() -> {
try {
// 1. 获取许可 (如果拿不到,这里会阻塞)
// 也可以使用 tryacquire() 来进行非阻塞尝试
semaphore.acquire();
system.out.println("线程 " + threadnum + " 拿到了令牌,正在处理业务...");
// 模拟业务耗时
timeunit.seconds.sleep(2);
} catch (interruptedexception e) {
thread.currentthread().interrupt();
} finally {
// 2. 关键:一定要在 finally 中释放许可!
system.out.println("线程 " + threadnum + " 处理完毕,归还令牌 ---");
semaphore.release();
}
});
}
executor.shutdown();
}
}
一般使用场景有这些,特别是框架特别喜欢用:
- 数据库连接池(限制最大连接数)
- 线程池任务提交限流
- 文件读写并发控制
- 服务接口的 qps 限流
平时使用的坑你要注意
场景 a:公平性 (fairness)
new semaphore(3, true) 会保证等待最久的线程最先拿到令牌。
- 优点: 避免线程饥饿。
- 缺点: 性能较差(因为要维护严格的队列顺序,增加了上下文切换开销)。通常后端高并发场景默认使用非公平模式以换取吞吐量。
场景 b:初始化为 0
semaphore 不一定要初始化为正数。如果你初始化为 new semaphore(0):
- 线程 a 调用
acquire()会立刻阻塞。 - 直到线程 b 调用
release(),线程 a 才会继续执行。 - 用途: 这种模式类似
countdownlatch或exchanger,用于线程间的单次信号通知。
常见坑
- 忘记 release: 如果在异常发生前没有释放,或者没写在 finally 块中,在这个 jvm 进程重启前,那个令牌就永久丢失了(类似内存泄漏),最终导致所有线程阻塞。所以千万要记住,写release在finally块!!
- release 滥用: release() 只是简单地将计数器 +1。如果你在没有 acquire 的情况下错误地调用了多次 release(),信号量的许可证数量会超过初始值(比如变成 5+1 = 6),导致限流失效。
和平时的reentrantlock / synchronized 的区别
使用用途和目的是它们最大的区别。
| 特性 | synchronized / reentrantlock | semaphore |
|---|---|---|
| 控制粒度 | 一次只允许一个线程进入临界区 | 可允许多个线程(≤许可数)同时进入 |
| 是否可重入 | 是(reentrantlock) | 否(许可不属于特定线程) |
| 主要用途 | 互斥访问 | 资源池、限流、并发控制 |
那么使用自定义计时器+锁+唤醒等待机制也是可以实现semaphore一样的功能,但是没必要,还容易出错。不如直接使用semaphore。
总结
semaphore就是用来限制同时访问统一资源的工具。
除了平时开发使用到semaphore,我们平时面试做有关于多线程的算法题也有可能用到噢,所以还是非常有必要熟悉semaphore的。
到此这篇关于一站式了解java中semaphore的基本用法的文章就介绍到这了,更多相关java semaphore用法内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论