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)都可以实现互斥,但它们语义不同。互斥锁强调“谁加锁谁解锁”,而信号量是纯粹的计数机制,不绑定拥有者。

BinarySemaphore


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 个线程在同时运行,其余线程在等待资源释放。

CountingSemaphore


4. 二值 vs 计数信号量对比

特性 二值信号量 计数信号量
值域范围 [0, 1] [0, N](N > 1)
可用资源数 1 N
是否支持互斥 ✅ 是 ❌ 否
使用场景 互斥访问、状态同步 资源池、限流、并发控制
是否绑定拥有者 ❌ 否 ❌ 否

5. 小结

  • 信号量是并发编程中非常基础且重要的同步机制
  • 二值信号量适用于需要互斥访问的场景,其本质是控制一个资源的唯一访问权。
  • 计数信号量适用于多个资源的共享访问,常用于资源池、线程池等场景。
  • 两者都使用 acquire()release() 方法进行操作,但其语义和适用范围不同。

踩坑提醒

  • 使用信号量时一定要确保 release() 总是会被调用,建议放在 finally 块中。
  • 不要混淆信号量和互斥锁的语义,虽然它们都能实现互斥,但用途和设计理念不同。

理解这两者的区别,有助于我们在不同场景中选择合适的同步机制,写出更高效、安全的并发代码。


原始标题:Binary Semaphores vs. Counting Semaphores