LeetCode 1195. 多线程 Fizz Buzz
题目描述


题意分析
四个线程分别负责输出
fizz、buzz、fizzbuzz和普通数字,整体仍要按整数1到n的顺序输出每一项。同时被
3和5整除时输出fizzbuzz;只被3整除时输出fizz,只被5整除时输出buzz,其余输出整数本身。线程调度可以任意交错,但每个数必须恰好由对应线程输出一次,后一个数不能抢到前面。
解法:四个信号量协调执行权
核心思路
[!blue]
让
number线程负责按数值递增顺序分派任务,其他三个线程等待自己负责的类别。设置四个执行许可,只有numberSem初始为一,其余为零,保证先由调度线程决定第一项交给谁。
number每轮先获取许可。当前是普通数字时,自己打印并归还numberSem;当前需要特殊文本时,把许可交给对应线程。下一轮开始前仍要获取numberSem,所以会等上一项真正完成打印后才能继续。专用线程获得自己的许可后只执行相应回调,打印结束才把许可归还
number。执行权始终只有一份,可能在同步对象中等待,也可能被某个回调的执行线程持有,因而不会出现两个数同时争抢输出顺序。分派时先判断
15的倍数,再判断3、5。专用线程的循环次数也必须一致:fizz跳过15的倍数,buzz同样跳过,fizzbuzz只等待这些共同倍数。它们的循环变量用于数清自己有多少次任务,全局次序仍由number控制。最后一个特殊任务可能在
number方法返回后才完成,因此整个调用要以四个方法全部完成为准。Java 信号量和 Go 容量为一的通道都能保存最后归还的许可,专用线程不会因为调度者不再领取而卡住,不需要额外结束消息。
解题步骤
- 创建四个同步对象,只有数字调度者初始持有一份许可。
number从1遍历到n,每轮先等待调度许可。- 依次按
15、3、5的整除关系选择专用线程;普通数则自己打印并归还许可。- 三个专用线程按各自实际负责的次数,反复等待、打印、归还调度许可。
- 全部数都输出完后,各线程完成规定次数并返回。
代码实现
class FizzBuzz {
private final int n;
private final java.util.concurrent.Semaphore numberSem = new java.util.concurrent.Semaphore(1);
private final java.util.concurrent.Semaphore fizzSem = new java.util.concurrent.Semaphore(0);
private final java.util.concurrent.Semaphore buzzSem = new java.util.concurrent.Semaphore(0);
private final java.util.concurrent.Semaphore fizzbuzzSem =
new java.util.concurrent.Semaphore(0);
public FizzBuzz(int n) {
this.n = n;
}
public void fizz(Runnable printFizz) throws InterruptedException {
for (int i = 3; i <= n; i += 3) {
// 十五的倍数交给专用线程,不为它多等待许可。
if (i % 5 == 0) {
continue;
}
fizzSem.acquire();
printFizz.run();
// 完成打印后归还调度许可,才可继续下一个数。
numberSem.release();
}
}
public void buzz(Runnable printBuzz) throws InterruptedException {
for (int i = 5; i <= n; i += 5) {
if (i % 3 == 0) {
continue;
}
buzzSem.acquire();
printBuzz.run();
// 完成打印后归还调度许可,才可继续下一个数。
numberSem.release();
}
}
public void fizzbuzz(Runnable printFizzBuzz) throws InterruptedException {
for (int i = 15; i <= n; i += 15) {
fizzbuzzSem.acquire();
printFizzBuzz.run();
// 完成打印后归还调度许可,才可继续下一个数。
numberSem.release();
}
}
public void number(java.util.function.IntConsumer printNumber) throws InterruptedException {
for (int i = 1; i <= n; i++) {
numberSem.acquire();
// 先判断共同倍数,避免被单独三或五的分支截走。
if (i % 15 == 0) {
fizzbuzzSem.release();
} else if (i % 3 == 0) {
fizzSem.release();
} else if (i % 5 == 0) {
buzzSem.release();
} else {
printNumber.accept(i);
// 完成打印后归还调度许可,才可继续下一个数。
numberSem.release();
}
}
}
}
type FizzBuzz struct {
n int
numberSem chan struct{}
fizzSem chan struct{}
buzzSem chan struct{}
fizzbuzzSem chan struct{}
}
func Constructor(n int) FizzBuzz {
fb := FizzBuzz{
n: n,
numberSem: make(chan struct{}, 1),
fizzSem: make(chan struct{}, 1),
buzzSem: make(chan struct{}, 1),
fizzbuzzSem: make(chan struct{}, 1),
}
// 调度线程从此通道获取执行许可,每次最多保留一枚。
fb.numberSem <- struct{}{}
return fb
}
func (fb *FizzBuzz) Fizz(printFizz func()) {
for i := 3; i <= fb.n; i += 3 {
// 十五的倍数交给专用线程,不为它多等待许可。
if i%5 == 0 {
continue
}
<-fb.fizzSem
printFizz()
// 调度线程从此通道获取执行许可,每次最多保留一枚。
fb.numberSem <- struct{}{}
}
}
func (fb *FizzBuzz) Buzz(printBuzz func()) {
for i := 5; i <= fb.n; i += 5 {
if i%3 == 0 {
continue
}
<-fb.buzzSem
printBuzz()
// 调度线程从此通道获取执行许可,每次最多保留一枚。
fb.numberSem <- struct{}{}
}
}
func (fb *FizzBuzz) Fizzbuzz(printFizzBuzz func()) {
for i := 15; i <= fb.n; i += 15 {
<-fb.fizzbuzzSem
printFizzBuzz()
// 调度线程从此通道获取执行许可,每次最多保留一枚。
fb.numberSem <- struct{}{}
}
}
func (fb *FizzBuzz) Number(printNumber func(int)) {
for i := 1; i <= fb.n; i++ {
<-fb.numberSem
// 先判断共同倍数,避免被单独三或五的分支截走。
if i%15 == 0 {
fb.fizzbuzzSem <- struct{}{}
} else if i%3 == 0 {
fb.fizzSem <- struct{}{}
} else if i%5 == 0 {
fb.buzzSem <- struct{}{}
} else {
printNumber(i)
// 调度线程从此通道获取执行许可,每次最多保留一枚。
fb.numberSem <- struct{}{}
}
}
}
复杂度分析
- 时间复杂度:不计回调与调度等待,同步工作 $O(n)$。
- 空间复杂度:$O(1)$,四个固定同步对象。
关键点总结
[!green]
- 调度线程决定类别与顺序,专用线程完成对应回调,执行权通过许可交接。
- 打印完成才归还,保证下一数字的放行不会先于当前输出。
- 每个专用线程等待次数必须等于派发次数,避免漏输出或结束后仍阻塞。
易错点总结
[!yellow]
- 先判断被
3或5整除,会把共同倍数提前分配给单一类别,漏掉fizzbuzz。fizz、buzz循环仍为共同倍数等待一次,会多等一个永远不会到来的许可。- 普通数字打印后忘记归还许可,调度者下一轮也会把自己阻塞。
- 专用线程在回调之前就归还许可,下一项可能抢先输出。
- Go 的通道改成无缓冲,初始放入许可或最后一次归还都可能阻塞。
- 只等待
number返回就认为全部完成,可能漏等最后一个仍在执行的专用回调。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 412. Fizz Buzz | 简单 | 输出规则相同,本题重点是让不同职责线程按同一个数值顺序同步输出。 |
| 1116. 打印零与奇偶数 | 中等 | 同样由当前数值条件决定交给哪个线程,本题按3和5的整除规则分派。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!