信号量 Semaphore
信号量 Semaphore
信号量(Semaphore)是一种同步机制,用于控制多个线程或进程对共享资源的访问,避免出现竞争条件或死锁问题。它主要在多线程或多进程编程中使用,尤其在并发编程中非常重要。
进程间通信采用共享内存的时候,通常需要信号量 semaphore 或者互斥体 mutex 进行进程间的并发协调。
看到共享内存的时候,我感觉可以直接操作内存真的好爽啊。可以搞出很多具有想象力的玩儿法。共享内存就是,很有想象力。
信号量的工作原理
- 信号量的定义
信号量是一个整型计数器,表示可以被访问的资源数量。- 当信号量的值大于零时,线程或进程可以访问资源,并使信号量的值减一。
- 当信号量的值等于零时,线程或进程会阻塞,直到信号量的值大于零。
- 当线程或进程释放资源时,信号量的值加一。
- 信号量的两种类型
- 计数信号量(Counting Semaphore):信号量的值可以大于 1,表示可以有多个线程同时访问资源。例如,限制某个资源的并发访问量为 N。
- 二进制信号量(Binary Semaphore):信号量的值只能是 0 或 1,类似于互斥锁(Mutex),用于实现互斥访问。
- 基本操作
信号量的主要操作包括:- Wait(P 操作):尝试获取信号量。如果信号量的值大于零,则减一并继续执行;否则,阻塞直到信号量的值大于零。
- Signal(V 操作):释放信号量,将信号量的值加一。如果有线程阻塞在该信号量上,则唤醒其中一个线程。
这里的 P 和 V 是荷兰语操作名的缩写,分别代表 "Proberen"(测试)和 "Verhogen"(增加)。
- 多线程环境中的工作流程
- 初始化信号量:设置初始值,通常等于可用资源的数量。
- 线程访问资源:线程在访问共享资源前执行 Wait 操作,减少信号量值。
- 线程释放资源:线程完成资源访问后执行 Signal 操作,增加信号量值。
信号量的实际应用
- 线程同步
信号量可以用于控制线程的执行顺序。例如,线程 A 必须在线程 B 之后执行,可以通过信号量阻塞线程 A,直到线程 B 释放信号量。 - 资源限制
信号量可以用于限制资源的访问量。例如,一个线程池中,信号量可以用来限制线程数。 - 生产者 - 消费者问题
信号量常用于解决经典的生产者 - 消费者问题:- 一个信号量表示缓冲区中的可用空间数量。
- 另一个信号量表示缓冲区中可用的产品数量。
信号量的优缺点
优点:
- 简单高效,适用于解决资源共享问题。
- 通过计数信号量,可以轻松实现多资源的管理。
缺点: - 编程复杂度较高,容易出错。
- 如果管理不当,可能导致死锁、资源泄漏等问题。
简单实践一下
当我们想要实现自己的线程锁的时候,可以参 semaphore 的设计
本来想自己写代码实践一下,结果发现,停止线程和启动线程要么得用 Object 的 wait 和 notify 方法,要么得用 Lock 的 Condition 方法,
Synchronized 关键字写法
class ProcessExample {
private static final Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Thread t1: Waiting...");
lock.wait(); // 阻塞当前线程
System.out.println("Thread t1: Resumed.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("Thread t2: Notifying...");
lock.notify(); // 唤醒线程 t1
}
});
t1.start();
try {
Thread.sleep(1000); // 确保 t1 先进入 wait 状态
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
Condition 写法
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ProcessExampleWithCondition {
private static final Lock lock = new ReentrantLock();
private static final Condition condition = lock.newCondition();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
lock.lock();
try {
System.out.println("Thread t1: Waiting...");
condition.await(); // 阻塞当前线程
System.out.println("Thread t1: Resumed.");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
Thread t2 = new Thread(() -> {
lock.lock();
try {
System.out.println("Thread t2: Signaling...");
condition.signal(); // 唤醒线程 t1
} finally {
lock.unlock();
}
});
t1.start();
try {
Thread.sleep(1000); // 确保 t1 先进入 await 状态
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
但是无论是哪种写法,都必须先获取锁,然后才能使用,而获取排他锁,本来就是 semaphore 应该干的事情,因此实际上实现不了,从这个角度看,AQS 的实现就很牛逼,不依赖 synchronized 关键字,从 0 实现了锁机制。