首页 > Java > java教程 > 正文

如何在Java中使用Semaphore控制并发

P粉602998670
发布: 2025-09-23 13:58:01
原创
804人浏览过
Semaphore用于限制并发访问数量,核心是许可计数机制。代码示例中创建了3个许可的信号量,控制最多3个线程同时执行,超出则阻塞等待。与ReentrantLock不同,前者限流并发数,后者保证独占访问。适用场景包括资源池管理、限流、I/O控制等,需注意释放许可、使用try-finally、避免跨线程释放及合理设置公平性与超时。

如何在java中使用semaphore控制并发

在Java中,Semaphore(信号量)是一个强大的并发工具,它允许我们精确控制同时访问某个资源或代码块的线程数量,本质上是一个计数信号量。它不是用来保护共享资源不被同时修改,而是用来限制资源的并发访问数量,防止系统过载或资源耗尽。简单来说,它就像一个“通行证发放机”,只有拿到通行证的线程才能继续执行,通行证发完了,其他线程就得等着。

Semaphore在Java并发包(java.util.concurrent)中扮演着关键角色,它提供了一种灵活的方式来管理那些只能被有限数量的线程同时访问的资源。其核心思想在于维护一个许可(permit)计数,线程在访问资源前需要获取许可,访问完成后则释放许可。如果许可数量不足,线程就会被阻塞,直到有其他线程释放许可。

import java.util.concurrent.Semaphore;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class SemaphoreDemo {

    // 假设我们有一个只能同时处理3个请求的服务
    private static final Semaphore SEMAPHORE = new Semaphore(3);

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 10; i++) {
            final int taskNum = i;
            executorService.execute(() -> {
                try {
                    System.out.println("任务 " + taskNum + " 尝试获取许可...");
                    SEMAPHORE.acquire(); // 获取一个许可
                    System.out.println("任务 " + taskNum + " 成功获取许可,开始执行。");
                    // 模拟业务处理
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    System.out.println("任务 " + taskNum + " 被中断。");
                } finally {
                    SEMAPHORE.release(); // 释放许可
                    System.out.println("任务 " + taskNum + " 执行完毕,释放许可。");
                }
            });
        }

        executorService.shutdown();
        try {
            executorService.awaitTermination(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("所有任务完成。");
    }
}
登录后复制

这段代码展示了Semaphore的基本用法。我们创建了一个初始许可数为3的Semaphore。这意味着,在任何给定时刻,最多只有3个线程可以同时执行 acquire()release() 之间的代码块。当有更多线程尝试 acquire() 时,它们会被阻塞,直到有其他线程调用 release() 释放许可。这就像是限定了入口的停车场,车位满了就得在外面排队。

Semaphore与ReentrantLock有何异同,何时选择Semaphore?

这确实是一个常被问到的好问题。很多初学者会觉得,Semaphore和ReentrantLock好像都能“锁”住东西,但它们的设计哲学和应用场景其实大相径庭。

立即学习Java免费学习笔记(深入)”;

异同点分析:

  1. 本质差异:

    • ReentrantLock 是一种互斥锁(Mutual Exclusion Lock),它确保在任何时刻,只有一个线程能持有锁并访问受保护的资源。它关注的是“谁能独占访问”。
    • Semaphore 是一种计数信号量,它关注的是“有多少个线程能同时访问”。它不强调独占,而是限制并发数量。你可以把它想象成一个资源池,比如数据库连接池,有N个连接,就允许N个线程同时使用。
  2. 许可/锁的粒度:

    • ReentrantLock 每次只能获取一个锁,并且必须由同一个线程释放。
    • Semaphore 可以获取一个或多个许可(通过 acquire()acquire(int permits)),并且可以在不同线程之间释放许可。虽然通常推荐获取许可的线程释放许可,但在某些特殊设计下,由其他线程代为释放也是可能的,但这需要非常小心。
  3. 使用场景:

    • ReentrantLock: 适用于需要对共享数据进行独占访问的场景,例如修改共享变量、更新数据结构等,避免数据不一致。
    • Semaphore: 适用于需要限制并发访问数量的场景,例如控制对有限资源的访问(连接池、线程池、文件句柄),或者流量控制,防止系统过载。

何时选择Semaphore?

我个人认为,当你面临以下情况时,Semaphore 往往是更合适的选择:

  • 资源池管理: 比如数据库连接池、线程池、对象池。你有一组有限的资源,希望多个线程能并发使用它们,但同时又不想所有线程一股脑儿地冲进去把资源耗尽。Semaphore能很好地控制并发使用的数量。
  • 流量控制/限流: 比如你调用一个第三方API,它有每秒请求次数的限制。你可以用Semaphore来限制自己应用的并发请求数量,避免超出对方的限制而被封禁。
  • 并发任务分阶段执行: 某些复杂任务可能需要分为几个阶段,每个阶段对并发数有不同的要求。Semaphore可以灵活地在不同阶段调整并发限制。
  • 生产者-消费者模式中的容量控制: 虽然BlockingQueue更常见,但在某些自定义的生产者-消费者实现中,Semaphore可以用来控制缓冲区(队列)的可用空间,限制生产者生产过快或消费者消费过慢。

简单来说,如果你需要“独占”某个东西,ReentrantLock 是你的朋友;如果你需要“限制多少人能同时用”某个东西,那么 Semaphore 就该登场了。

使用Semaphore时,有哪些常见的陷阱和最佳实践?

Semaphore虽然好用,但用起来也有些坑,一不小心就可能导致程序出问题。这里我总结了一些常见的陷阱和对应的最佳实践,希望能帮大家避开雷区。

如知AI笔记
如知AI笔记

如知笔记——支持markdown的在线笔记,支持ai智能写作、AI搜索,支持DeepseekR1满血大模型

如知AI笔记 27
查看详情 如知AI笔记

常见陷阱:

  1. 忘记释放许可(release()): 这是最常见的错误,没有之一。如果在 acquire() 之后,因为异常或者其他逻辑分支没有执行到 release(),那么这个许可就永远丢失了。随着时间的推移,所有许可都会被“吃掉”,最终导致所有尝试 acquire() 的线程永远阻塞,系统瘫痪。这就像你借了一本书没还,图书馆的书就少了一本,长此以往,大家都没书可借了。
  2. 在不持有许可的情况下调用 release() 尽管 Semaphore 不会像 ReentrantLock 那样抛出 IllegalMonitorStateException,但如果你在没有 acquire() 的情况下调用 release(),会增加许可计数。这可能会导致许可数超过初始值,从而允许更多的线程并发访问,打破了你最初设定的并发限制,造成资源争抢或系统过载。
  3. 死锁: 虽然 Semaphore 本身不容易直接导致传统意义上的死锁,但在复杂的并发场景中,如果多个线程需要获取不同类型的 Semaphore 许可,并且获取顺序不一致,就可能形成循环等待,进而导致死锁。例如,线程A持有许可X等待许可Y,线程B持有许可Y等待许可X。
  4. 公平性问题: Semaphore 默认是非公平的,这意味着当有许可释放时,等待时间最长的线程不一定能优先获取到许可,这可能导致某些线程“饿死”或响应延迟过高。

最佳实践:

  1. 使用 try-finally 块确保许可释放: 毫无疑问,这是最重要的实践。将 acquire() 放在 try 块之前,将 release() 放在 finally 块中,可以确保即使在业务逻辑执行过程中发生异常,许可也能被正确释放。

    SEMAPHORE.acquire(); // 放在try块之外,如果获取失败,就不会进入try块
    try {
        // 核心业务逻辑
        // ...
    } finally {
        SEMAPHORE.release();
    }
    登录后复制

    当然,如果你希望在获取许可失败时(例如被中断)也能处理,可以把 acquire() 放在 try 块里,但要确保异常处理得当。

  2. 合理设置许可数量: 许可数量直接决定了并发度。设置过高可能导致资源争抢和系统过载,设置过低则可能导致资源利用率不足。这需要根据实际的系统资源、业务负载和性能测试结果来确定。我通常会从一个保守的数字开始,然后逐步调整。

  3. 考虑公平性(Fairness): 如果你的应用对线程的响应顺序有严格要求,或者担心线程饿死,可以在创建 Semaphore 时指定为公平模式:new Semaphore(permits, true)。公平模式会保证等待时间最长的线程优先获取许可,但通常会带来一些性能开销。在大多数情况下,非公平模式就足够了,因为它吞吐量更高。

  4. 超时机制: 在调用 acquire() 时,考虑使用 tryAcquire()acquire(long timeout, TimeUnit unit) 方法。这些方法允许线程尝试获取许可,如果指定时间内未能获取到,则放弃等待或进行其他处理,避免无限期阻塞。这对于需要高可用和快速响应的系统尤其重要。

    if (SEMAPHORE.tryAcquire(5, TimeUnit.SECONDS)) {
        try {
            // 业务逻辑
        } finally {
            SEMAPHORE.release();
        }
    } else {
        System.out.println("未能获取许可,超时处理或重试。");
    }
    登录后复制
  5. 避免跨线程释放许可: 尽管技术上可行,但在一个线程中 acquire(),在另一个线程中 release() 是一种高风险的操作,极易出错且难以调试。除非你有非常明确且经过严格验证的理由,否则请保持 acquire()release() 在同一个线程中执行。

总之,Semaphore是一个强大的工具,但需要小心翼翼地使用。记住“获取了就一定要释放”这个黄金法则,并结合 try-finally 块,就能规避大部分问题。

Semaphore在实际高并发系统中有哪些典型应用场景?

在真实世界的高并发系统中,Semaphore可以说是一个“幕后英雄”,它默默地在许多关键位置发挥着作用,帮助系统稳定运行并优化资源利用。

  1. 数据库连接池的并发控制: 这是最经典的场景之一。数据库连接是宝贵的资源,创建和销毁连接都有开销,而且数据库本身能处理的并发连接数也是有限的。连接池会维护一定数量的数据库连接,当应用程序需要连接时,从池中“借用”,用完后“归还”。Semaphore在这里的作用就是限制同时从连接池中获取连接的线程数量。

    • 实现方式: 连接池初始化时,创建一个与连接数相同许可的Semaphore。每次从池中获取连接前,调用 semaphore.acquire();归还连接时,调用 semaphore.release()。这样就能确保不会有超过连接池容量的线程同时占用数据库连接。
  2. 限制对第三方API的并发请求: 很多外部服务(如支付接口、短信平台、地图服务等)都会对单个客户端的请求频率或并发数做限制。如果我们的系统并发请求量过大,可能会被对方封禁IP或返回错误。

    • 实现方式: 在调用第三方API的代码外部包裹一层Semaphore。例如,如果某个API限制每秒最多100个请求,或者最多同时处理10个并发请求,我们就可以创建一个许可数为10的Semaphore,确保我们的请求不会超出这个限制。
  3. 控制文件或IO操作的并发数: 磁盘I/O操作通常比CPU操作慢得多,过多的并发文件读写可能会导致磁盘I/O成为瓶颈,甚至拖垮系统。

    • 实现方式: 对于那些可能导致高I/O负载的操作(如大文件上传下载、日志写入等),可以使用Semaphore来限制同时进行的I/O线程数量,确保I/O子系统不会过载。这在处理大量数据导入导出时尤为有用。
  4. 资源密集型任务的并发调度: 有些任务(例如图片处理、视频转码、复杂计算)会消耗大量的CPU或内存资源。如果同时运行太多这样的任务,可能会导致服务器负载过高,影响其他服务的响应。

    • 实现方式: 创建一个Semaphore,其许可数与服务器能够高效处理的资源密集型任务数量相匹配。只有获取到许可的线程才能启动这些高消耗任务。
  5. 构建自定义的限流器: 除了上述具体场景,Semaphore也可以作为构建更复杂限流器的基础组件。例如,结合定时任务,可以实现滑动窗口或令牌桶算法的简易版本,用于更精细的流量控制。

这些场景无一例外都体现了Semaphore的核心价值:在面对有限资源或需要避免系统过载时,它提供了一种优雅且高效的并发控制机制。它不像锁那样粗暴地“独占”,而是像一个交通管制员,合理地分配“通行权”,让系统既能充分利用资源,又能保持稳定。

以上就是如何在Java中使用Semaphore控制并发的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号