【问题标题】:Is my multi-threaded "fizz buzz" implementation thread safe?我的多线程“嘶嘶声”实现线程安全吗?
【发布时间】:2019-04-05 18:47:19
【问题描述】:

出于教育目的,我正在使用多线程实现经典的“嘶嘶声”问题。

“嘶嘶声”游戏是:

指定先走的玩家说出数字“1”,然后每个玩家依次数一个数字。然而,任何能被三整除的数字都被单词 fizz 代替,任何能被五整除的数字都被单词 Buzz 代替。能被两者整除的数字会变成嗡嗡声

在我的实现中,我有 4 个线程:

  • 如果数字不是 3 或 5 的倍数,则第一个线程打印数字并递增当前计数器。
  • 第二个线程打印“嘶嘶”...
  • 第三个线程打印“嗡嗡声”...
  • 第四线程打印“嘶嘶声”...

我不使用任何锁定和线程同步机制。 我的多线程“嘶嘶声”实现线程安全吗?如果不是,为什么? 我在“可疑”地方的实现代码中添加了 cmets。

我的实现:

package threads;    
import java.util.function.IntFunction;    
public class FizzBuzzGameRunner {

    // not volatile
    // if other thread updates currentNum and current thread will see old (cached) value
    // nothing bad can happen, just burn some CPU cycles uselessly
    private int currentNum = 1;

    public static void main(String... args) throws InterruptedException {
        FizzBuzzGameRunner fizzBuzzGame = new FizzBuzzGameRunner();
        startAll(
                fizzBuzzGame.createRunnable(n -> (n % 3 != 0 && n % 5 != 0) ? String.valueOf(n) : null),
                fizzBuzzGame.createRunnable(n -> (n % 3 == 0 && n % 5 != 0) ? "fizz" : null),
                fizzBuzzGame.createRunnable(n -> (n % 3 != 0 && n % 5 == 0) ? "buzz" : null),
                fizzBuzzGame.createRunnable(n -> (n % 3 == 0 && n % 5 == 0) ? "fizz buzz" : null)
        );
        Thread.sleep(1000);
    }

    private static void startAll(Runnable... workers) {
        for (Runnable w : workers) {
            Thread t = new Thread(w);
            t.setDaemon(true);
            t.start();
        }
    }

    private Runnable createRunnable(IntFunction<String> singleStep) {
        return () -> {
            while (true) {
                int currNum = this.currentNum;
                // no synchronization
                String result = singleStep.apply(currNum);
                if (result != null) {
                    //Even without synchronization this block will be
                    //executed maximum by single thread simultaneously.
                    //Because each thread increments this.currentNum as part of that action,
                    //but no other thread will increment for the same value.
                    System.out.println(result);
                    this.currentNum++;
                }
            }
        };
    }
}

我知道我的例子完全是人为的。实现多线程“Fizz Buzz”算法启发了一本名著,为“编码面试”做准备。我只是想证明书中的例子(需要有 4 个线程)可以在不使用同步和锁的情况下解决。

【问题讨论】:

  • @matt 我认为这个想法是(尽管问题可能需要更多解释......)只有满足其条件的线程才会增加 currNum。因此,每个线程什么都不做,直到其他线程将 currNum 提高到适当的值(在每种情况下,都会导致其适当性质的输出,“fizz”或“buzz”或“fizzbuzz”或数字)。它比最初看起来更棘手。
  • @davmac 在我的示例中,一个线程在 A 获取值和 B 获取值之间更新 curNum。例如。 currentNum = 9,A 读取该值并将 curNum 分配给 9,线程 C 更新 currentNum,tad B 分配 curNum = 10;现在 A 和 B 都有正确的值,这是一场比赛。哦,你说 A 在 C 更新之前不能得到 9,否则 C 会得到 9。
  • @matt 我不关注。 “正确的价值”是什么意思?每个线程都有一组不相交的值,它们将对其进行操作。作为该操作的一部分,每个线程都会增加 curNum,但没有其他线程会增加相同的值。在您的示例中,如果 A 将 curNum 读取为 9,然后 C 更新 curNum,那么一定是因为 C 将 curNum 视为具有它将起作用的值,这意味着 9 和 C 看到的值都不是 A 将起作用的值开,所以不管它有哪一个。
  • “正确值”是特定线程操作的值。对,所以 C 必须看到 8 并采取行动,然后将值从 9 更新为 10,如果 C 看到旧值,可能会发生这种情况。由于线程 C 是唯一将 8 修改为 9 的线程,因此它看不到陈旧的值。
  • @matt 对,我想我们现在在同一个页面上。

标签: java multithreading


【解决方案1】:

好的,我更改了它,而不是打印输出,而是将其发布到队列中。我让队列检查该值是否满足条件。它总是中断,因为队列正在填满。或者,您可以将输出写入文件并运行数千次以查看它是否全部损坏。 它对我来说永远不会坏。虽然不能证明它是无种族的,但反过来会显示出种族。

import java.util.function.IntFunction;
import java.util.concurrent.ArrayBlockingQueue;


public class FizzBuzzGameRunner {
    static ArrayBlockingQueue<String> output = new ArrayBlockingQueue<>(100);
    private int currentNum = 1;

    public static void main(String... args) throws InterruptedException {

        FizzBuzzGameRunner fizzBuzzGame = new FizzBuzzGameRunner();
        startAll(
                fizzBuzzGame.createRunnable(
                        n -> {
                            if (n % 3 != 0 && n % 5 != 0) {
                                return String.valueOf(n);
                            }
                            return null;
                        }),
                fizzBuzzGame.createRunnable(
                        n -> {
                            if (n % 3 == 0 && n % 5 != 0) {
                                return "fizz";
                            }
                            return null;
                        }),
                fizzBuzzGame.createRunnable(
                        n -> {
                            if (n % 3 != 0 && n % 5 == 0) {
                                return "buzz";
                            }
                            return null;
                        }),
                fizzBuzzGame.createRunnable(
                        n -> {
                            if (n % 3 == 0 && n % 5 == 0) {
                                return "fizz buzz";
                            }
                            return null;
                        })
        );

        int n = 1;
        String s;
        while(true){
            s = output.take();
            if (n % 3 != 0 && n % 5 != 0) {
                if(!String.valueOf(n).equals(s)){
                    break;
                }
            }
            if (n % 3 == 0 && n % 5 != 0) {
                if(!"fizz".equals(s)) break;
            }
            if (n % 3 != 0 && n % 5 == 0) {
                if(!"buzz".equals(s)) break;
            }
            if (n % 3 == 0 && n % 5 == 0) {
                if(!"fizz buzz".equals(s)) break;
            }
            n++;
        }

        System.out.println(s + " out of order after" + n);
    }

    private static void startAll(Runnable... workers) {
        for (Runnable w : workers) {
            Thread t = new Thread(w);
            t.setDaemon(true);
            t.start();
        }
    }

    private Runnable createRunnable(IntFunction<String> singleStep) {
        return () -> {
            while (true) {
                int currNum = this.currentNum;
                // no synchronization
                // even if other thread updates currentNum nothing bad can happen
                String result = singleStep.apply(currNum);
                if (result != null) {
                    //even without synchronization this block will be 
                    //executed maximum by single thread simultaneously
                    try{
                        output.put(result);
                    } catch(InterruptedException e){
                        break;
                    }
                    this.currentNum++;
                }
            }
        };
    }



}

【讨论】:

  • 如果您让您的版本运行约 100,000 次并将输出传输到文件并检查输出,我想您会发现一些失败的行。为什么你认为问题出在 output.offer 上?
  • 示例行“output.offer(result)”有问题。如果队列已满,offer 将忽略元素。我改变了“output.offer(结果);”到“输出.put(结果);”一切都按预期工作(运行很长时间)
  • 所以你正在做某种自旋锁。线程可能会错过一次读取而什么也不做,或者它们可以读取并更新它们的值。
【解决方案2】:

它不是无种族的(这是我认为你真正要问的),因为当另一个线程写入时,线程从 currentNum 读取,没有任何同步。不保证每个线程都能看到最新的值 - 每个线程都会看到它自己上次写入的值,或者任何其他线程此后写入的任何值。

这可能意味着您最终在任何线程中都没有前向进展,因为每个线程可能根本看不到其他线程所做的更改。你可以使用AtomicInteger 来解决这个问题。

我也不确定this.currentNum++; 的效果是否可以保证以与源线程中相同的方式被其他线程看到。我怀疑理论上,输出和增量可以重新排序,例如:

              this.currentNum++;
              System.out.println(result);

这可能导致输出不一致。

【讨论】:

  • 我认为“this.currentNum++”行只能由单线程同时到达,因为只有单线程“result = singleStep.apply(currNum)”不能同时为空。所以 this.currentNum++;并且 System.out.printLn 永远不会被多个线程执行
  • @Palladium 为什么你认为 "result = singleStep.apply(currNum)" 不能在多个线程中同时为空?
  • @Palladium 您有 4 个线程同时运行,没有任何同步可以阻止这些线程同时访问 currNum,或阻止线程处理相同的 currNum ,或确保输出顺序为适当的
  • @davmac 因为“singleStep.apply(currNum);”只有当 currNum 对当前线程“合适”时才能返回非 null,并且只能同时发生在一个线程上。
  • @Palladium 很恶心。您可能是对的,增量不能从不同的线程同时发生。但仍然存在重大问题。我已经重新措辞了答案。
猜你喜欢
  • 1970-01-01
  • 2011-02-20
  • 1970-01-01
  • 2011-04-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-20
  • 1970-01-01
相关资源
最近更新 更多