当前位置:首页 > 文章列表 > 文章 > java教程 > JavaSemaphore并发控制全解析

JavaSemaphore并发控制全解析

2025-10-03 19:45:53 0浏览 收藏

Java并发编程中,`Semaphore` 信号量是一种强大的工具,用于精确控制对共享资源的并发访问数量,有效防止系统过载。它通过维护一个许可计数器实现限流,线程在访问资源前需获取许可,访问后释放。本文将通过代码示例,深入剖析 `Semaphore` 的核心机制和用法,展示如何创建拥有特定许可数的信号量,控制并发线程数量,超出限制则阻塞等待。同时,对比 `Semaphore` 与 `ReentrantLock` 的异同,前者侧重并发数控制,后者保证独占访问。最后,总结 `Semaphore` 在资源池管理、限流、I/O控制等高并发场景下的典型应用,并强调使用 `try-finally` 确保许可释放、避免跨线程释放等最佳实践,助你安全高效地运用 `Semaphore` 构建稳定可靠的Java并发系统。

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好像都能“锁”住东西,但它们的设计哲学和应用场景其实大相径庭。

异同点分析:

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

常见陷阱:

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

好了,本文到此结束,带大家了解了《JavaSemaphore并发控制全解析》,希望本文对你有所帮助!关注golang学习网公众号,给大家分享更多文章知识!

包子漫画新地址及免费阅读方法包子漫画新地址及免费阅读方法
上一篇
包子漫画新地址及免费阅读方法
PDF转Word如何保留书签?
下一篇
PDF转Word如何保留书签?
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ChatExcel酷表:告别Excel难题,北大团队AI助手助您轻松处理数据
    ChatExcel酷表
    ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
    3193次使用
  • Any绘本:开源免费AI绘本创作工具深度解析
    Any绘本
    探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
    3405次使用
  • 可赞AI:AI驱动办公可视化智能工具,一键高效生成文档图表脑图
    可赞AI
    可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
    3436次使用
  • 星月写作:AI网文创作神器,助力爆款小说速成
    星月写作
    星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
    4543次使用
  • MagicLight.ai:叙事驱动AI动画视频创作平台 | 高效生成专业级故事动画
    MagicLight
    MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
    3814次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码