1. 信号量概述
在并发编程中,多个线程或进程可能同时访问共享资源。如果不加控制,就容易引发数据不一致、竞态条件(race condition)等问题。为了解决这些问题,操作系统和编程语言提供了多种同步机制,其中 信号量(Semaphore) 是一种非常经典且常用的机制。
信号量本质上是一个带有整数值的变量,用于控制对共享资源的访问。它有两个核心操作:
acquire()
/P()
:尝试获取一个资源。如果当前值大于 0,减少 1 并继续执行;否则阻塞,直到值大于 0。release()
/V()
:释放一个资源,将信号量值增加 1,并唤醒一个等待的线程。
根据信号量值的范围,可以分为两类:
- 二值信号量(Binary Semaphore)
- 计数信号量(Counting Semaphore)
接下来我们分别讲解它们的原理与区别。
2. 二值信号量(Binary Semaphore)
二值信号量的值只能是 0 或 1,因此它只能表示两种状态:资源是否可用。
✅ 二值信号量常用于实现互斥访问(Mutual Exclusion),也就是说,它能保证同一时刻只有一个线程可以进入临界区(Critical Section)。
示例代码(Java):
import java.util.concurrent.Semaphore;
public class BinarySemaphoreExample {
private static final Semaphore binarySemaphore = new Semaphore(1);
public static void main(String[] args) {
Runnable task = () -> {
try {
binarySemaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 进入临界区");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 离开临界区");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
binarySemaphore.release();
}
};
new Thread(task, "线程-A").start();
new Thread(task, "线程-B").start();
}
}
输出示例:
线程-A 进入临界区
线程-A 离开临界区
线程-B 进入临界区
线程-B 离开临界区
⚠️ 注意:虽然二值信号量和互斥锁(Mutex)都可以实现互斥,但它们语义不同。互斥锁强调“谁加锁谁解锁”,而信号量是纯粹的计数机制,不绑定拥有者。
3. 计数信号量(Counting Semaphore)
计数信号量的值可以是 0 到某个最大值 N,表示有多个资源可用。它允许多个线程同时访问资源,最多 N 个。
✅ 计数信号量常用于资源池、连接池、线程池等场景,例如控制数据库连接数量、限制并发线程数等。
示例代码(Java):
import java.util.concurrent.Semaphore;
public class CountingSemaphoreExample {
private static final Semaphore countingSemaphore = new Semaphore(3); // 允许最多3个线程同时访问
public static void main(String[] args) {
Runnable task = () -> {
try {
countingSemaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 正在使用资源");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 释放资源");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countingSemaphore.release();
}
};
for (int i = 1; i <= 5; i++) {
new Thread(task, "线程-" + i).start();
}
}
}
输出示例:
线程-1 正在使用资源
线程-2 正在使用资源
线程-3 正在使用资源
线程-1 释放资源
线程-4 正在使用资源
线程-2 释放资源
线程-5 正在使用资源
...
可以看到,最多只有 3 个线程在同时运行,其余线程在等待资源释放。
4. 二值 vs 计数信号量对比
特性 | 二值信号量 | 计数信号量 |
---|---|---|
值域范围 | [0, 1] | [0, N](N > 1) |
可用资源数 | 1 | N |
是否支持互斥 | ✅ 是 | ❌ 否 |
使用场景 | 互斥访问、状态同步 | 资源池、限流、并发控制 |
是否绑定拥有者 | ❌ 否 | ❌ 否 |
5. 小结
- 信号量是并发编程中非常基础且重要的同步机制。
- 二值信号量适用于需要互斥访问的场景,其本质是控制一个资源的唯一访问权。
- 计数信号量适用于多个资源的共享访问,常用于资源池、线程池等场景。
- 两者都使用
acquire()
和release()
方法进行操作,但其语义和适用范围不同。
✅ 踩坑提醒:
- 使用信号量时一定要确保
release()
总是会被调用,建议放在finally
块中。 - 不要混淆信号量和互斥锁的语义,虽然它们都能实现互斥,但用途和设计理念不同。
理解这两者的区别,有助于我们在不同场景中选择合适的同步机制,写出更高效、安全的并发代码。