为什么这个LeetCode的 Fizz Buzz多线程实现每次运行的输出都不一样?

编程语言 2026-07-10

我在调试一个LeetCode的 Fizz Buzz多线程解法:

https://leetcode.com/problems/fizz-buzz-multithreaded/

下面的实现对我来说在逻辑上看起来是安全的,但在LeetCode上,它对 n = 50 在不同运行之间会产生不同的输出。在本地测试中,我也无法稳定复现这个问题。

我设定的不变量是:

  • 只有其 idturn 匹配的线程才能继续执行
  • 所有其他线程都在各自的信号量上阻塞
  • 打印后,活动线程恰好释放下一个被期望的线程的信号量

因此我在寻求两种可能中的一种:

  1. 给出一个具体的交错执行序列,能够破坏这个不变量,或
  2. 解释为什么这个不变量本身是错误的

我暂时不寻求替代实现,我希望具体理解这个版本的同步逻辑哪里出了问题。

我有意选择的一个设计约束是:在每个输出步骤只计算下一步的动作一次,而不是在每个工作线程中独立地重新评估是否可被整除。

主方法据说创建了4 个线程,这些线程对公开方法中的其中一个只调用一次。

class FizzBuzz {
    private final Semaphore sFizz = new Semaphore(0, false);
    private final Semaphore sBuzz = new Semaphore(0, false);
    private final Semaphore sFizzBuzz = new Semaphore(0, false);
    private final Semaphore sNumber = new Semaphore(0, false);
    private volatile int index = 1;
    private final int max;
    private volatile Id turn;
    private final Object mutex = new Object();

    public FizzBuzz(int n) {
        this.max = n;
        turn = calculateTurn(index);
    }

    private enum Id {
        FIZZ, BUZZ, FIZZBUZZ, NUMBER
    }

    private Integer markReady(Id id) throws InterruptedException {
        if (id != turn) {
            switch (id) {
                case NUMBER -> sNumber.acquire();
                case BUZZ -> sBuzz.acquire();
                case FIZZ -> sFizz.acquire();
                case FIZZBUZZ -> sFizzBuzz.acquire();
            }
        }

        synchronized (mutex) {
            if (index > max) {
                return null;
            }
            int turnIndex = index++;
            if (index <= max) {
                turn = calculateTurn(index);
            }
            return turnIndex;
        }
    }

    private Id calculateTurn(int _index) {
        if (_index % 3 == 0 && _index % 5 == 0) {
            return Id.FIZZBUZZ;
        } else if (_index % 3 == 0) {
            return Id.FIZZ;
        } else if (_index % 5 == 0) {
            return Id.BUZZ;
        } else {
            return Id.NUMBER;
        }
    }

    private void releaseWaiting(Id id) {
        if (id != turn) {
            switch (turn) {
                case FIZZBUZZ -> sFizzBuzz.release();
                case FIZZ -> sFizz.release();
                case BUZZ -> sBuzz.release();
                case NUMBER -> sNumber.release();
            }
        }
    }

    // printFizz.run() outputs "fizz".
    public void fizz(Runnable printFizz) throws InterruptedException {
        while (index <= max) {
            try {
                var num = markReady(Id.FIZZ);
                if (num == null) return;
                printFizz.run();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                releaseWaiting(Id.FIZZ);
            }
        }
        releaseAll();
    }

    // printBuzz.run() outputs "buzz".
    public void buzz(Runnable printBuzz) throws InterruptedException {
        while (index <= max) {
            try {
                var num = markReady(Id.BUZZ);
                if (num == null) return;
                printBuzz.run();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                releaseWaiting(Id.BUZZ);
            }
        }
        releaseAll();

    }

    // printFizzBuzz.run() outputs "fizzbuzz".
    public void fizzbuzz(Runnable printFizzBuzz) throws InterruptedException {
        while (index <= max) {
            try {
                var num = markReady(Id.FIZZBUZZ);
                if (num == null) return;
                printFizzBuzz.run();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                releaseWaiting(Id.FIZZBUZZ);
            }
        }

        releaseAll();
    }

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void number(IntConsumer printNumber) throws InterruptedException {
        while (index <= max) {
            try {
                var num = markReady(Id.NUMBER);
                if (num == null) return;
                printNumber.accept(num);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                releaseWaiting(Id.NUMBER);
            }
        }
        releaseAll();
    }

    private void releaseAll() {
        sFizzBuzz.release();
        sFizz.release();
        sBuzz.release();
        sNumber.release();
    }
}

解决方案

下面给出一个示例的交错执行,错误地输出 1, 2, 4, fizz 而不是 1, 2, fizz, 4

初始状态是除了 number() 线程已经打印了1 和2 并且刚刚释放了 fizz() 线程的信号量外,其他所有线程都在它们的信号量上阻塞。

number() 线程 fizz() 线程 索引 轮到 输出
releaseWaiting(): sFizz.release() markReady(): sFizz.acquire() 3 FIZZ 1, 2
releaseWaiting() 返回,这个线程回到了 number(),但在 markReady() 在while循环的下一次迭代中执行之前
markReady(): int turnIndex = index++ 4
markReady(): turn = calculateTurn(index) NUMBER
markReady() 返回3,这个线程回到 fizz(),但在 printFizz.run() 执行之前
markReady(): if (id != turn) 轮到是NUMBER,因此此处判定为false
markReady(): int turnIndex = index++ 5
markReady(): turn = calculateTurn(index) BUZZ
markReady() returns 4, back in number()
number(): printNumber.accept(num) 1, 2, 4
fizz(): printFizz.run() 1, 2, 4, fizz

在它为自己的轮次输出之前,fizz() 线程将 turn 更新为NUMBER,这使得 number() 线程可以先输出,因为它不需要等待自己的信号量。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章