1. 生产者与消费者(Producer‑Consumer)
场景
想象一家超市的“自助结账机”。
| 角色 | 说明 |
|---|---|
| 生产者 | 把刚进来的货物(数据)放进小车(共享缓冲区) |
| 消费者 | 取走货物后卖给顾客(使用数据) |
- 小车不是无限大,它有 N 个格子(缓冲区)。
- 生产者跟消费者不可能同时操作同一个格子。
潜在问题
- 空箱子:当小车空了,消费者就找不到任何东西,全部等着 “吃饭”。
- 满箱子:当小车装满了,生产者就再也放不下,全部停下来等。
如果没有任何同步手段,往往会出现
- 频繁抢占:生产者和消费者争着抢同一个格子,导致写错数据。
- 死循环:比方说生产者一次把多个格子搞满,消费者连条都收不到,两个都停不下来。
典型解决法:信号量 + 互斥锁
| 变量 | 含义 | 初始值 | 必要同步操作 |
|---|---|---|---|
| empty (计数信号量) | 空格子数 | N | empty.wait()(生产者先等空),empty.signal()(消费者放一格空) |
| full (计数信号量) | 已装格子数 | 0 | full.wait()(消费者先等满),full.signal()(生产者装完一格) |
| mutex (互斥锁) | 保护缓冲区 | mutex.lock()/unlock() |
流程
-
生产者
empty.wait(); // 等空 (若无空格子就阻塞) mutex.lock(); // 拿到盒子做事,确保同一时间只有一个人拿 put(item); // 把货物放进去 mutex.unlock(); full.signal(); // 通知有新货 -
消费者
full.wait(); // 等满 (若没货就阻塞) mutex.lock(); item = get(); // 取走货物 mutex.unlock(); empty.signal(); // 通知有空格子
简单说:
empty 代表还有多长的 “摆位”(空位)可以放东西。
full 代表有多少 “摆位”已经被占用(有货)。
两者各自忙着“等槽”与“放槽”,互不冲突。
2. 读者‑写者(Readers‑Writers)
场景
想象一个共享的图书库。
| 角色 | 说明 |
|---|---|
| 读者 | 只想看书,不需要做任何写入 |
| 写者 | 需要 挑选 书(稍微改一点)并写入新内容 |
- 多名读者同时进来可以 一起 看同一本书。
- 写者需要 排空书架:读者全部收回,写者拿走后再重新把书架换回来。
潜在问题
| 现象 | 结果 |
|---|---|
| 读写冲突 | 写者读完再写,读者看到半成品。 |
| 写者饿死 | 如果读者不断来,写者永远得不到空读者桌。 |
| 读者饿死 | 写者过多,读者只能等待。 |
典型解决法:读者计数 + 互斥锁 + 写者信号量
先想想:
为了让读者能“并行”,我们只需给【读书区】建一道闸门,读者闸门可同时通行,但写者闸门必须完全封闭。
方案要满足:
- 读者可并行
- 写者独占
- 读者 & 写者 互不与对方“打孔”
| 变量 | 含义 | 初始值 | 用法 |
|---|---|---|---|
| mutex (互斥锁) | 保护 readCount |
mutex.lock()/unlock() |
|
| writeLock (写信号量) | 写者排他 | 1 | writeLock.wait()/signal() |
| readCount (整型) | 当前读者数量 | 0 | 读者进入/离开时增/减 |
流程
-
读者
mutex.lock(); // 保护读者计数 readCount += 1; if (readCount == 1) {writeLock.wait(); // 第一个读者阻塞写者 } mutex.unlock();// ----- 读操作开始 ----- read(); // ----- 读操作结束 ----- mutex.lock(); readCount -= 1; if (readCount == 0) {writeLock.signal(); // 最后一个读者放行写者 } mutex.unlock(); -
写者
writeLock.wait(); // 等 读者 离开 write(); // 写操作 writeLock.signal(); // 放行
解释:
writeLock 是“写者的门”,只有 零 人读时可以通过。
读者进来时:
- 第一个读者点开门(
writeLock.wait()),随后其它读者直接穿行(不再触碰门)。- 最后一个读者点关门,放行。
这样就既让读者并行,又保证写者写前全量读者都离开,写后读者可以立刻进入。
3. 餐桌哲学家(Dining Philosophers)
场景
5 位哲学家围坐在桌子旁,桌面正好有 5 支叉子。
- 每顿饭:哲学家需要 两支叉子(左右两边)才能吃面。
- 叉子是共有资源,哲学家只是 “拿” 和 “放”,不是 “写” 或 “读”。
潜在问题:死锁
为什么会死锁?
想像所有哲学家同时拿起左右的一只叉子:
- 每个人都拿起自己的右边叉子(先起手)。
- 接下来每个人都要拿左边的叉子,却发现它被邻居抢走。
- 所有哲学家都在 等待,却没有人能继续动。
这就是经典“所有进程都等待对方释放资源”的死锁。
简易解决方案
| 方案 | 思路 | 关键点 |
|---|---|---|
| 资源编号(顺序) | 把叉子编号 1…N,所有哲学家 先拿编号较小的 再拿编号较大的 | 只需要保证 “拿 1 和 2” 的顺序相同即可(没有回环) |
| 奇偶策略 | 令偶数哲学家先拿左叉子,奇数先拿右叉子 | 根本不可能所有人都按同一顺序拿,避免死锁 |
| 外部控制者(如“服务员”) | 一位服务员只允许 最多 N-1 位哲学家同时拿叉子,再来的人等待 | 确保至少有一支叉子空闲,死锁不可能 |
| 随机放下 | 当哲学家拿到一只叉子后,如果放不到另一只就立刻把已拿的放下,稍后再试 | 破坏循环等待,可能导致无穷循环(但不会死锁) |
最常见演示实现:先把 第 5 位哲学家 改成 “先拿右叉子后拿左叉子”,其余三位保持原方式。
这样三位哲学家并行,而第五位在等待时必定至少有一只叉子是空闲的,谁也得不到饥饿死锁的机会。
代码(Java 伪作示例)
class Philosopher implements Runnable {private final int id;private final Fork left, right;public Philosopher(int id, Fork left, Fork right) {this.id = id;this.left = left;this.right = right;}public void run() {try {for (;;) {think(); // 思考if (id % 2 == 0) { // 偶数先拿左,奇数先拿右left.pick();right.pick();} else {right.pick();left.pick();}eat(); // 吃饭left.put(); // 放左右叉子right.put();}} catch (InterruptedException ignored) { }}
}
Fork类内部使用
synchronized或ReentrantLock。
小结:同步的三大核心概念
| 概念 | 解释 | 例子 |
|---|---|---|
| 互斥(Mutual Exclusion) | 同一时刻只能有一个线程访问共享资源。 | mutex 只让一个线程“打开门”。 |
| 序列化(Ordering) | 规定资源获取的顺序,避免循环等待。 | 资源编号、奇偶顺序。 |
| 等待与唤醒 | 线程在资源不可用时挂起,有资源时被唤醒。 | 信号量的 wait()/signal()。 |
只要能把 “谁先拿?谁等?” 和 “谁等待” 这两件事做得彻底,任何同步问题都能化解。
