一站式了解Java中Semaphore的基本用法

 更新时间:2026年02月09日 10:52:32   作者:想用offer打牌  
本文主要介绍了一站式了解Java中Semaphore的基本用法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

引言

我们今天一起来了解一下JUC的同步工具类-Semaphore的基本用法。

什么是Semaphore(信号量)

Semaphore (信号量)java.util.concurrent 包下非常有用的一个并发工具类。你可以把它理解为用于控制同时访问特定资源的线程数量的工具。它维护了一组“许可”(permits),线程在访问受保护资源前必须先获取一个许可,使用完后再释放该许可。

场景引入

想象一个只有 5 个车位的停车场(共享资源):

  1. Permits (许可证/令牌): 初始化时,Semaphore 拥有 5 个令牌。
  2. Acquire (获取): 车辆(线程)进入时,先领 1 个令牌。如果没有令牌了(计数为0),车辆就得在门口排队(阻塞)。
  3. 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 才会继续执行。
  • 用途: 这种模式类似 CountDownLatchExchanger,用于线程间的单次信号通知

常见坑

  1. 忘记 Release: 如果在异常发生前没有释放,或者没写在 finally 块中,在这个 JVM 进程重启前,那个令牌就永久丢失了(类似内存泄漏),最终导致所有线程阻塞。所以千万要记住,写release在finally块!!
  2. Release 滥用: release() 只是简单地将计数器 +1。如果你在没有 acquire 的情况下错误地调用了多次 release(),信号量的许可证数量会超过初始值(比如变成 5+1 = 6),导致限流失效。

和平时的ReentrantLock / synchronized 的区别

使用用途和目的是它们最大的区别。

特性synchronized / ReentrantLockSemaphore
控制粒度一次只允许一个线程进入临界区可允许多个线程(≤许可数)同时进入
是否可重入是(ReentrantLock)否(许可不属于特定线程)
主要用途互斥访问资源池、限流、并发控制

那么使用自定义计时器+锁+唤醒等待机制也是可以实现Semaphore一样的功能,但是没必要,还容易出错。不如直接使用Semaphore。

总结

Semaphore就是用来限制同时访问统一资源的工具。

除了平时开发使用到Semaphore,我们平时面试做有关于多线程的算法题也有可能用到噢,所以还是非常有必要熟悉Semaphore的。

到此这篇关于一站式了解Java中Semaphore的基本用法的文章就介绍到这了,更多相关Java Semaphore用法内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring Boot自动配置源码实例解析

    Spring Boot自动配置源码实例解析

    Spring Boot作为Java领域最为流行的快速开发框架之一,其核心特性之一就是其强大的自动配置机制,下面这篇文章主要给大家介绍了关于Spring Boot自动配置源码的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-08-08
  • java读取文件内容,解析Json格式数据方式

    java读取文件内容,解析Json格式数据方式

    这篇文章主要介绍了java读取文件内容,解析Json格式数据方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • 解决static类使用@Value获取yml文件获取不到的问题

    解决static类使用@Value获取yml文件获取不到的问题

    在静态类中直接使用@Value注解无法获取yml文件中的配置,解决方案是在工具类Utils中创建静态的setter方法,并从外部类ServiceClass中调用这个方法来设置值,这种方法通过外部调用来间接设置静态变量的值,从而成功读取yml配置
    2024-09-09
  • java datetime数据类型去掉时分秒的案例详解

    java datetime数据类型去掉时分秒的案例详解

    在Java中,如果我们想要表示一个日期而不包括时间(时分秒),我们通常会使用java.time包中的LocalDate类,这篇文章主要介绍了java datetime数据类型去掉时分秒,需要的朋友可以参考下
    2024-06-06
  • java获取指定开始时间与结束时间之间的所有日期

    java获取指定开始时间与结束时间之间的所有日期

    这篇文章主要为大家详细介绍了java获取指定开始时间与结束时间之间的所有日期,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-05-05
  • java中MultipartFile类型转为File类型的4种方法

    java中MultipartFile类型转为File类型的4种方法

    Spring提供了一个MultipartFile接口来处理文件上传,但有时候我们需要将MultipartFile转换为File来进行一些特定的操作,比如保存文件到本地或者进行文件的处理等,这篇文章主要给大家介绍了关于java中MultipartFile类型转为File类型的4种方法,需要的朋友可以参考下
    2024-09-09
  • Java中的Set、List、Map的用法与区别介绍

    Java中的Set、List、Map的用法与区别介绍

    这篇文章主要介绍了Java中的Set、List、Map的用法与区别,需要的朋友可以参考下
    2016-06-06
  • springAOP的三种实现方式示例代码

    springAOP的三种实现方式示例代码

    这篇文章主要介绍了springAOP的三种实现方式,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-07-07
  • Java把Map转为对象的实现代码

    Java把Map转为对象的实现代码

    在项目开发中,经常碰到map转实体对象或者对象转map的场景,工作中,很多时候我们可能比较喜欢使用第三方jar包的API对他们进行转化,但这里,我想通过反射的方式对他们做转化,感兴趣的同学跟着小编来看看吧
    2023-08-08
  • Java简单计算圆周率完整示例

    Java简单计算圆周率完整示例

    这篇文章主要介绍了Java简单计算圆周率,结合完整实例形式分析了Java计算圆周率的原理与操作技巧,代码备有较为详尽的注释便于理解,需要的朋友可以参考下
    2018-05-05

最新评论