# 并发理论:管程和信号量

# 介绍
在操作系统中,信号量(Semaphore)和管程(Monitor)是两种最核心的同步机制,用于解决多线程或多进程在并发执行时的互斥(不能同时访问临界资源)和同步(按特定顺序执行)问题
# 信号量
信号量由操作系统巨擘 Dijkstra 提出,本质上是一个整型变量,配合两个原子操作(不可中断的操作)来控制并发。这两个操作最初被称为 P 和 V(源自荷兰语),现在通常被称为 wait 和 signal
# 核心机制
计数器的含义:信号量的值
P 操作 (wait / down):想要消费资源时执行。 S = S - 1 如果 S < 0,说明没资源了,该线程进入阻塞队列等待。
V 操作 (signal / up):释放资源时执行。 S = S + 1 如果 S <= 0,说明有线程在排队,唤醒阻塞队列中的一个线程。

# 信号量的分类
二值信号量:值只能是 0 或 1。作用类似于一把锁,主要用于实现互斥
计数信号量:值可以大于 1。用于管理多个同类资源(例如:限流、连接池)
缺点:信号量非常强大、灵活,但就像 C 语言的手动内存管理一样,极易出错。程序员必须小心翼翼地配对 P 和 V 操作。如果漏写一个 V,就会导致死锁;如果写错顺序,整个同步机制就会崩溃
# 管程
为了解决信号量“容易写错”的问题,霍尔(C.A.R. Hoare)和汉森(Brinch Hansen)提出了管程。
管程是一种面向对象思想的同步机制。它把共享变量和对这些变量的所有操作(函数)全封装在一个特殊的“盒子”(类/模块)里
# 核心特性
管程中的被迫互斥:管程有一个硬性规则——在任意时刻,只能有一个线程在管程内部执行。其他想进入管程的线程必须在管程外的入口队列(Entry Queue)中排队。这直接由编译器或语言底层保证,程序员不需要手动加锁。
条件变量:为了解决线程在管程内部“因为某些条件不满足而必须等待”的问题,管程引入了条件变量。
wait() 操作:当线程发现条件不满足(例如缓冲区满了),它就调用 x.wait(),主动释放管程的控制权,并进入条件
signal() 操作:当另一个线程让条件满足了(例如拿走了一个数据),它就调用 x.signal(),唤醒在

# 管程在Java中的应用
Java 语言的设计深度依赖管程模型。在 Java 中,每一个对象(Object)在诞生时,其内部都会隐式附带一个管程(通常被称为 Monitor / 内置锁)
# 隐式管程:synchronized 关键字
这是 Java 中最常见的管程实现。当你使用 synchronized 关键字时,Java 编译器和 JVM 会自动帮你处理管程的进入和退出
public class BoundBuffer {
private final Object lock = new Object(); // 充当管程的对象
private int count = 0;
public void produce() throws InterruptedException {
synchronized(lock) { // 进入管程(自动获取锁)
while (count == 10) {
lock.wait(); // 条件不满足,线程释放管程并进入等待队列
}
count++;
lock.notifyAll(); // 唤醒其他等待的线程
} // 退出管程(自动释放锁)
}
}
入口队列(Entry Set):所有等待进入 synchronized 代码块的线程都会在这里排队
等待队列(Wait Set):调用了 lock.wait() 的线程会释放锁,并进入这个专门的条件等待队列,直到被 notify() 唤醒
# 显式管程:ReentrantLock + Condition
为了提供更灵活的控制(例如支持多个等待条件),Java 在 java.util.concurrent.locks 包中提供了显式管程
ReentrantLock 充当管程的互斥锁。Condition 充当管程的条件变量(一个管程可以绑定多个条件)
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition(); // 队列不满条件(独立等待队列1)
Condition notEmpty = lock.newCondition(); // 队列不空条件(独立等待队列2)
synchronized就是基于管程实现的,只包含一个同步队列,一个等待队列 AQS也是基于管程实现的,只包含一个同步队列,但是可以包含多个等待队列