如何理解java的信号量

想要理解好信号量这个概念,你可以从「限制资源」这几个字开始。在并发编程的领域里,会有一些限制资源使用的工具,比如我们熟悉的Java线程池就是,但是我们今天不讲线程池,我们讲信号量。

信号量也是限制资源使用的一种很好的手段。

对于初学并发编程的程序员,我特别想说的是:在并发编程的领域里,限制资源使用是我们的核心命题之一。

这个不急着懂,等后续你经历的实战项目多了/故障也处理多了,自然会对这句话深有体会的。

理解信号量,大多数文章都从里面的「许可」这个基础概念开始,但是我们今天不是先讲这个。因为许可只是基础特性,

最关键的特性其实是「解耦」。

你没有看错,就是解耦。什么意思呢?

就是说信号量是只认数量,不认线程的,任何线程都可以往这个计数器里加数或减数,信号量本身既不记录、也不关心、更不校验这些操作来自哪个线程。

它就是一个共享的公共计数器而已。

我们用java 17写段代码来验证这一点。

java 复制代码
import java.util.concurrent.Semaphore;

public class SemaphoreDecouplingDemo {
    public static void main(String[] args) throws InterruptedException {
        // 初始许可数为0,强制形成跨线程的释放-获取依赖
        Semaphore semaphore = new Semaphore(0);

        // 线程A:只负责消费(获取)许可
        Thread consumer = new Thread(() -> {
            try {
                System.out.println("[消费者线程Consumer]尝试获取许可...阻塞等待中");
                semaphore.acquire(); // 阻塞等待,直到有许可可用
                System.out.println("[消费者线程Consumer]成功获取许可!开始处理任务");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "Consumer");

        // 线程B:只负责生产(释放)许可,与线程A完全无关
        Thread producer = new Thread(() -> {
            try {
                // 模拟异步准备资源的耗时
                Thread.sleep(500);
                System.out.println("[生产者线程Producer]资源准备完毕,释放许可");
                semaphore.release(); // 由另一个线程创建许可
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "Producer");

        consumer.start();
        producer.start();

        // 等待两个线程执行完毕
        consumer.join();
        producer.join();
    }
}

执行结果如下:

text 复制代码
[消费者线程Consumer]尝试获取许可...阻塞等待中
[生产者线程Producer]资源准备完毕,释放许可
[消费者线程Consumer]成功获取许可!开始处理任务

注意上面的代码,consumer线程里只有一个acquire操作,而producer线程里只有一个release操作。

这就是解耦,它并不像ReentrantLock那样,得自己lock和unlock,也即是ReentrantLock是需要遵守「谁加锁谁解锁」这个规则的。

不信的话,我们将上面的代码换成ReentrantLock,一执行立刻报错,代码如下:

java 复制代码
import java.util.concurrent.locks.ReentrantLock;

public class LockCouplingDemo {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();

        // 线程A:只负责加锁
        Thread locker = new Thread(() -> {
            System.out.println("[Locker] 尝试获取锁...");
            lock.lock();
            System.out.println("[Locker] 成功获取锁!");
        }, "Locker");

        // 线程B:只负责解锁(与加锁线程完全无关)
        Thread unlocker = new Thread(() -> {
            try {
                // 等待确保Locker已经持有锁
                Thread.sleep(500);
                System.out.println("[Unlocker] 尝试释放锁...");
                lock.unlock(); // ⚠️ 跨线程解锁,必然抛出异常
                System.out.println("[Unlocker] 成功释放锁!");
            } catch (IllegalMonitorStateException e) {
                System.out.println("[Unlocker] 释放锁失败了!异常是: " + e.getClass().getSimpleName());
                System.out.println("   异常信息: " + e);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "Unlocker");

        locker.start();
        unlocker.start();

        locker.join();
        unlocker.join();
    }
}

运行的结果如下:

text 复制代码
[Locker] 尝试获取锁...
[Locker] 成功获取锁!
[Unlocker] 尝试释放锁...
[Unlocker] 释放锁失败了!异常是: IllegalMonitorStateException
   异常信息: java.lang.IllegalMonitorStateException

从上面的运行结果你就可以知道,unlocker这个线程是无法释放锁的,因为这个锁只能有之前加锁的locker线程,本人去释放。

好,理解了信号量这个解耦特性后,我们可以来开始说一下「许可」这个基础特性了。

所谓「许可」,本质上就是信号量内部维护的那个整数计数器。它不代表任何具体的物理资源。获取许可就是让计数器减一,释放许可就是让计数器加一;当计数器归零时,后续的获取操作就会被阻塞,直到有新的许可被释放。

这个我们上面提到的,信号量只认数量,不认线程的。

信号量的应用是非常广泛的,或者信号量的这种思路应用非常的广泛,比如我们熟悉的限流功能,就是利用这个特性来实现的。

比如阿里的限流框架Sentinel,其底层原理正是使用Semaphore记录某个资源或API接口当前的并发调用数。当并发数达到设定阈值时,直接拒绝新的请求(或者走降级逻辑)。

而像Dubbo/Tomcat等,只要涉及到资源限制使用的,也都会采用类似的思路。

另外,在这里,我想跟初入程序员这行的新手说一个学习技术的方法。

就是你得「一块一块知识去体系化的学习」,而不是每次只学习一个点而已。

比如说:

  • 关于java里各种同步工具,你要知道是在处理线程安全的问题;
  • 而像线程饥饿,活锁,你要知道,它是活跃性的问题;
  • 。。。。。。。

按照主题去学习相关的知识点,然后再总结和思考和记录,长期坚持下去,你的技术就会慢慢的进步了。

且当你遇到类似主题相关的难题时,你的解决思路会宽广很多,也会敏捷很多的。

技术高手解决问题快,正是这个原因,加油吧,年轻的程序员。

相关推荐
东风破_1 小时前
NestJS 快速入门:先别背装饰器,把一条请求跑明白
后端
晴空蓝天1 小时前
MDC traceId 全链路日志追踪:Spring Boot 3.5 里把日志串成一条线
java·spring boot·后端·python
泡海椒1 小时前
JQuick-Excel 项目定位与适用场景:声明式 Java Excel 的边界
java·开发语言·excel
明月_清风1 小时前
干了 6 年前端,我是怎么一步步转型到 AI 的?
前端·后端·ai编程
AC赳赳老秦1 小时前
财报附注表格精准提取:OpenClaw 从 PDF 年报附注挖掘隐藏明细,补齐财务分析维度
java·汇编·c++·python·青少年编程·deepseek·openclaw
顽疲1 小时前
Java vue 养老系统源码详解:楼宇床位 care_space 与房态图 Spring Boot 实践
后端
箓维1 小时前
线程的优缺点,与进程的关联和差异
java·服务器·笔记
曹牧1 小时前
Spring MVC:@RequestMapping
java·spring·mvc
Lyy1 小时前
DevOps平台 — 第十三篇:规划的设计与实现
后端·devops