当在关键部分中显示System.out.println时,多个线程显示例外值

时间:2017-07-23 17:07:52

标签: java multithreading concurrency

我有以下代码:

public class MyApp {
    public static void main(String[] args) throws InterruptedException {
        SharedResource sharedResource = new SharedResource();

        Runnable first = () -> {
            sharedResource.increment(10000);
        };

        Runnable second = () -> {
            sharedResource.increment(10000);
        };

        Thread thread = new Thread(first, "FirstThread");
        Thread thread2 = new Thread(second, "SecondThread");
        thread.start();
        thread2.start();
        thread.join();
        thread2.join();
        System.out.println("The value of counter is " + sharedResource.getCounter());
    }
}

使用此课程:

public class SharedResource {
    private int counter;

    public void increment(int times) {
        for (int x=1; x<=times;x++) {
            counter++;
            // System.out.println(counter);
        }
    }

    public void decrement() {
        counter--;
    }

    public int getCounter() {
        return counter;
    }
}

我很好奇为什么会一直这样。

从增量方法中删除System.out.println()时,

的总值
System.out.println("The value of counter is " + sharedResource.getCounter());

是随机的 - 这是例外,因为多个线程共享相同的counter

但是,当在增量方法上显示System.out.println(counter);时,代码似乎不再具有多线程问题。

计数器的最终结果总是20,000,因为代码从每个线程中丢失了10,000次,因此除外。任何人都可以解释我为什么会这样吗?

1 个答案:

答案 0 :(得分:5)

这是由于比赛窗口非常小。

默认系统输出是PrintStream,它是线程安全的:

public void println(int x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

所以基本上线程执行以下操作:

  1. 递增计数器(〜数十ns)
  2. 等待上一个线程释放锁定,获取它并打印到控制台(〜毫秒,慢1000倍)
  3. 转到1
  4. 当你的关键部分比非关键部分长1000倍时,你的线程基本上是序列化的,并且计数器更新重叠的概率变得非常小,系统输出没有什么特别之处

    证明方法:

    1. 您可以编写PrintStream的线程安全实现:

      public class NullPrintStream extends PrintStream {
        public NullPrintStream() {
            // Utility constant from apache-commons
            super(NullOutputStream.NULL_OUTPUT_STREAM);
        }
      
        @Override
        public void println(int x) {
           // No synchronization here
        }
      }
      
    2. 然后通过System.setOut(new NullPrintStream())将其设置为系统输出,结果将再次开始拍打。

      1. 要在开头提供更大的竞赛窗口,您可以在latch上同步runnables,使它们几乎同时启动:

        CountDownLatch latch = new CountDownLatch(1);
        Runnable first = () -> {
          try {
            latch.await();
            sharedResource.increment(10000);
          }
          catch (Exception e) {
          }
        };
        // ... start your threads here
        latch.countDown();
        
      2. 然后,如果您运行此示例几次,您会看到类似的内容(请注意我将其打印到System.err,因为我已覆盖System.out

        计数器的值是20000

        计数器的值是19996

        计数器的值是19994

        计数器的值是19999

        计数器的值是20000