Semaphore案例

wen java案例 2

Semaphore信号量实战案例全解析

目录导读

  1. Semaphore是什么?为什么需要它?
  2. 核心原理:从“许可证”到“流量闸门”
  3. 经典案例一:数据库连接池限流
  4. 经典案例二:电商秒杀系统的抢购保护
  5. 经典案例三:文件上传服务并发控制
  6. 高频面试问答(含源码级解析)
  7. 性能调优与避坑指南
  8. 何时用Semaphore,何时用其他并发工具

Semaphore是什么?为什么需要它?

在多线程编程中,我们经常遇到“资源有限,但请求无限”的困境,数据库连接池最多只有10个连接,但有100个线程同时请求;秒杀系统瞬间涌入10万用户,但库存只有100件。

Semaphore案例

如果不对这些并发请求加以控制,轻则系统响应变慢,重则直接崩溃。Semaphore(信号量) 就是Java并发包中专门用来解决这类“限流”问题的工具,它通过维护一组许可证(Permits),控制同时访问特定资源的线程数量。

synchronizedLock(它们只允许一个线程进入临界区)不同,Semaphore允许N个线程同时进入,非常适合“资源池化”场景。


核心原理:从“许可证”到“流量闸门”

Semaphore内部维护一个计数器(state),每次acquire()获取许可时减1,release()归还许可时加1,当计数器为0时,后续线程阻塞等待。

关键方法:

  • acquire() / acquire(int permits):阻塞获取许可(可响应中断)
  • tryAcquire():非阻塞尝试获取,失败立即返回false
  • release() / release(int permits):释放许可
  • availablePermits():查看当前剩余许可数

两种模式:

  • 公平模式:FIFO顺序发放许可,避免线程饥饿(构造参数new Semaphore(3, true)
  • 非公平模式:抢占式,吞吐量更高(默认)

经典案例一:数据库连接池限流

场景描述:假设系统维护一个固定大小的数据库连接池(比如5个连接),多个业务线程需要并发访问数据库,如果不限制,可能创建过多连接导致数据库崩溃。

实现方案

public class DBPool {
    private final Semaphore semaphore;
    private final List<Connection> connections;
    public DBPool(int poolSize) {
        this.semaphore = new Semaphore(poolSize, true);
        this.connections = new ArrayList<>();
        // 初始化连接...
    }
    public Connection getConnection() throws InterruptedException {
        semaphore.acquire(); // 获取许可,没有则等待
        return connections.remove(0); // 从池中取连接
    }
    public void returnConnection(Connection conn) {
        connections.add(conn);
        semaphore.release(); // 归还许可
    }
}

效果:无论多少线程请求,同时只有5个线程能获得连接,其余线程优雅等待。


经典案例二:电商秒杀系统的抢购保护

场景描述:秒杀活动开始瞬间,大量请求涌入,后端服务需要防止订单系统被高并发打垮,同时保证库存扣减原子性。

实现方案(伪代码):

// 在秒杀接口入口处加入Semaphore限流
Semaphore seckillLimit = new Semaphore(100); // 同时最多处理100个秒杀请求
public Result seckill(Long userId, Long productId) {
    if (!seckillLimit.tryAcquire()) {
        return Result.error("系统繁忙,请稍后再试"); // 快速失败,保护系统
    }
    try {
        // 1. 校验库存
        // 2. 扣减库存(Redis或DB原子操作)
        // 3. 创建订单
        return Result.success();
    } finally {
        seckillLimit.release(); // 释放许可
    }
}

关键优化:使用tryAcquire而非acquire,避免请求大量阻塞堆积,而是快速返回“系统繁忙”提示,保护核心业务。


经典案例三:文件上传服务并发控制

场景描述:文件上传服务需要限制同时处理的文件数量,防止内存溢出和带宽耗尽。

实现方案

public class UploadService {
    private static final Semaphore UPLOAD_LIMIT = new Semaphore(20); // 同时最多20个上传任务
    public void handleUpload(UploadRequest request) {
        if (UPLOAD_LIMIT.tryAcquire(2, TimeUnit.SECONDS)) { // 等待最多2秒
            try {
                processFile(request); // 执行上传
            } finally {
                UPLOAD_LIMIT.release();
            }
        } else {
            throw new BusyException("当前上传任务过多,请稍后重试");
        }
    }
}

此案例亮点:结合tryAcquire(timeout),在限定时间内获取许可,超时则快速失败,保证用户体验。


高频面试问答(含源码级解析)

Q1:Semaphore与CountDownLatch有什么区别?

  • Semaphore:控制同时访问资源的线程数量,许可可复用(获取-归还-再获取)
  • CountDownLatch:等待N个事件完成后放行,计数只能做减法,不能重复使用
  • 一句话记忆:Semaphore管“同时能有几个”,CountDownLatch管“什么时候开始”

Q2:Semaphore 获取许可后,如果线程异常退出,会有什么问题?

如果线程在acquire()之后抛出异常,而没有在finallyrelease(),会导致许可泄漏,最终所有线程阻塞,因此必须使用try-finally结构。

Q3:Semaphore能实现互斥锁吗?

可以。new Semaphore(1) 即为二元信号量,效果等同于synchronized,但不保证线程所有权(可由其他线程释放),更适合“信号传递”场景。

Q4:公平模式一定比非公平模式好吗?

不一定,公平模式避免了线程饥饿,但会降低系统吞吐量,对于高并发场景且对延迟不敏感,推荐非公平模式;对于必须保证请求顺序的场景(如打印任务),推荐公平模式。


性能调优与避坑指南

常见坑位

  1. 忘记释放许可 → 死锁风险极高,必须用finally
  2. 释放过多许可 → 超出初始容量,导致限流失效
  3. 在循环中acquire → 可能导致线程长时间等待,注意中断响应
  4. 与事物结合时 → 事务提交后释放许可,否则事务期间资源被占用

调优建议

  • 初始许可数设置为预估并发峰值的80%,留20%缓冲
  • tryAcquire替代acquire,配合超时策略
  • 监控availablePermits()queueLength(),动态调整池大小
  • 在高并发短任务场景下,可以通过SemaphoreThreadPoolExecutor组合使用,形成“双层限流”

何时用Semaphore,何时用其他并发工具?

场景 推荐工具 原因
限制同时访问资源数 Semaphore 天然支持多许可
互斥访问 synchronized/Lock 保证所有权,可重入
多线程等待单点完成 CountDownLatch 一次性放行
循环屏障同步 CyclicBarrier 可重置,多线程互相等待
读写分离 ReentrantReadWriteLock 性能优化于读多写少
限流+快速失败 Semaphore + tryAcquire 保护系统稳定

最后核心要义:Semaphore是“流量阀门”,不是“业务逻辑”,正确使用它,能让你在千万级并发面前保持系统丝滑流畅;错误使用,则可能引入隐性死锁,记住“acquire必须配对release”,并且优先使用tryAcquire超时版本,这是生产环境无数血泪教训总结出的铁律。


延伸思考:在Spring Boot中,如何用@Async + Semaphore实现方法级限流?欢迎在评论区留言讨论你的生产级实现方案。

上一篇Phaser案例

下一篇CyclicBarrier案例

抱歉,评论功能暂时关闭!