想要理解好信号量这个概念,你可以从「限制资源」这几个字开始。在并发编程的领域里,会有一些限制资源使用的工具,比如我们熟悉的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里各种同步工具,你要知道是在处理线程安全的问题;
- 而像线程饥饿,活锁,你要知道,它是活跃性的问题;
- 。。。。。。。
按照主题去学习相关的知识点,然后再总结和思考和记录,长期坚持下去,你的技术就会慢慢的进步了。
且当你遇到类似主题相关的难题时,你的解决思路会宽广很多,也会敏捷很多的。
技术高手解决问题快,正是这个原因,加油吧,年轻的程序员。