【问题标题】:How to demonstrate java multithreading visibility problems?如何演示java多线程可见性问题?
【发布时间】:2011-02-16 17:49:47
【问题描述】:

如果从多个线程访问 Java 中的变量,则必须确保它们被安全地发布。这通常意味着使用synchronizedvolatile

我的印象是,我的一些同事并不认真对待这个问题,因为他们“以前从未听说过volatile,而且他们的程序已经运行了多年”。

所以我的问题是:

谁能提供一个示例 Java 程序/sn-p,它可靠地显示数据可见性问题。

我认为运行一个程序并看到意外的 NPE 或过时的变量值会比仅仅理论上的解释更有帮助,而这无法证明。

非常感谢您的帮助!

更新:再次强调这一点。我已阅读 Java Concurreny in Practice 并且知道理论上存在可见性问题的示例。我正在寻找的是一种真正展示它们的方法。我不确定这实际上是否可行,但也许有一个 jvm 配置或类似的东西允许它。

【问题讨论】:

    标签: java multithreading visibility


    【解决方案1】:

    通过删除操作修改示例here,我想出了一个在我的环境中始终失败的示例(线程永远不会停止运行)。

    // Java environment:
    // java version "1.6.0_0"
    // OpenJDK Runtime Environment (IcedTea6 1.6.1) (6b16-1.6.1-3ubuntu3)
    // OpenJDK 64-Bit Server VM (build 14.0-b16, mixed mode)
    public class Test2 extends Thread {
        boolean keepRunning = true;
        public static void main(String[] args) throws InterruptedException {
            Test2 t = new Test2();
            t.start();
            Thread.sleep(1000);
            t.keepRunning = false;
            System.out.println(System.currentTimeMillis() + ": keepRunning is false");
        }
        public void run() {
            while (keepRunning) 
            {}
        }
    }
    

    请注意,此类问题在很大程度上取决于编译器/运行时/系统。特别是编译器可以确定添加指令以从内存中读取变量,即使它不是易失性的——所以代码可以工作——,vm和jit可以优化从内存中读取并只使用寄存器,甚至处理器可以重新排序指令——这不会影响这种情况,但在其他多线程情况下,如果修改了多个变量,它可能会影响来自其他线程的感知状态。

    【讨论】:

    • 这如何暴露数据可见性问题? “始终失败”是什么意思?
    • 太棒了!非常感谢。此示例适用于我的 Windows 笔记本电脑和带有 client-vm 的 Solaris 10,但在带有 server-vm 的 Solaris 和 64 位 Linux 上会失败(即永远运行)。正是我想要的。最好的事情是,在 Solaris 上,-client/-server 之间的切换使它要么总是失败,要么总是运行。这应该在演示文稿中推动这一点。 :)
    • @aioobe,这不是可见性问题吗,因为如果字段keepRunning 的更新状态对第二个线程不可见,它将永远运行?我完全错过了什么吗?
    【解决方案2】:

    强调使用 volatile 重要性的最常见示例是 while(keepRunning) 示例:

    public class Test extends Thread {
    
        boolean keepRunning = true;
    
        public static void main(String[] args) throws InterruptedException {
            Test t = new Test();
            t.start();
            Thread.sleep(1000);
            t.keepRunning = false;
            System.out.println(System.currentTimeMillis() + ": keepRunning is false");
        }
    
        public void run() {
            while (keepRunning) 
                System.out.println(System.currentTimeMillis() + ": " + keepRunning);
        }
    }
    

    由于keepRunning 可能(有效地)保存在运行while 循环的线程的缓存中,因此该程序可能会在keepRunning 设置为false 很久之后为keepRunning 打印“true”。

    但是请注意没有可靠的方法来公开竞争条件。 (请参阅我的其他答案。)此示例可能在某些情况下在硬件/操作系统/jvm 的某些组合上公开它。

    【讨论】:

    • 在我的机器上打印: 1273221858765: true ... 1273221859140: true 1273221859156: keepRunning is false 正如预期的那样。有没有办法真正引发可见性问题?我正在寻找的是一个 sn-p,它实际上显示了问题,并且在理论上没有它。
    • @Joe23,首先,删除对系统时间的调用,它们不是必需的。您只需要显示在另一个线程打印错误之后打印出“真”。其次,您可能无法在您的开发人员机器/PC 上创建可见性问题(更少的内核和全部)。我有一个双核,并且能够每隔数百次尝试就展示一次可见性问题。
    • 多次运行测试,你会经常遇到一个故障。现在的问题是测试是否真的有效,因为打印输出中可能存在竞争条件。即使使用正确的代码——使用 volatile——也可能是两个线程同时调用println 并且输出可能是无序的。并发难,主要是因为测试太难了。
    • @Tim,System.currentTimeMillis() 用于确保在 while 条件下未读取 keepRunning,将 keepRunning 设置为 false,然后打印。然而,正如@David 指出的那样,这是一个不好的例子。
    【解决方案3】:

    谁能提供一个示例 Java 程序/sn-p,可靠地显示数据可见性问题。

    不,没有显示数据可见性问题的可靠示例。

    原因是程序的任何有效执行 volatile 也是同一程序的有效执行 volatile。 (相反的情况显然不是真的!)

    【讨论】:

      【解决方案4】:

      我有一段代码给你:

      package test;
      
      public class LoopTest {
      
       private boolean done = false;
      
       /**
        * @param args
        */
       public void start() {
        System.out.println(System.getProperty("java.vm.name"));
        System.out.println(System.getProperty("java.version"));
        for (int i = 0; i < 100; i++) {
         startNewThread();
        }
      
        try {
         Thread.sleep(1000);
        } catch (InterruptedException e) {
         e.printStackTrace();
        }
        done = true;
        System.out.println("forcing end");
       }
      
       private void startNewThread() {
        new Thread(new Runnable() {
      
         public void run() {
          long i = 0;
          while(!done) {
           i++;
           if(i % 100L == 0) {
            System.out.println("still working " + i);
           }
          }
          System.out.println("ending " + i);
         }
      
        }).start();
       }
      
       public static void main(String[] args) {
        new LoopTest().start();
       }
      
      }
      

      这个以 JVM 服务器模式运行的示例在我的机器上生成了这个输出:

      ..
      ..
      ending 14100
      still working 14800
      ending 14800
      still working 26500
      ending 26500
      still working 18200
      ending 18200
      still working 9400
      ending 9400
      still working 1300
      ending 1300
      still working 59500
      ending 59500
      still working 1700
      still working 75400
      ending 75400
      still working 33500
      ending 33500
      still working 36100
      ending 36100
      still working 121000
      ending 121000
      still working 3000
      ending 3000
      ending 1700
      still working 5900
      ending 5900
      still working 7800
      ending 7800
      still working 7800
      ending 7800
      still working 6800
      ending 6800
      still working 5300
      ending 5300
      still working 9100
      still working 10600
      ending 10600
      still working 9600
      ending 9600
      still working 10000
      ending 10000
      ending 9100
      still working 1700
      ending 1700
      ..
      ..
      

      看看“结束#”语句:我认为它们都有一个 100 的倍数的数字,这不太可能发生。我的解释是存在可见性问题,导致线程仍然读取 done == false,尽管它已经更新为 true。在使用 "still working #" 语句调用同步的 System.out.println() 方法后,线程读取完成的更新值并退出。

      或者有人看到我的代码/解释中有错误吗?

      【讨论】:

      • 嗯,即使在字段声明中添加了 'volatile' 后,所描述的效果仍然存在。因此,它似乎更有可能是时间问题,不是吗?我假设代码在 sysout 中花费了更多时间,因此它更有可能被停止,而 i % 100.
      【解决方案5】:

      让他们阅读 Java 并发大师 Brian Goetz 的书Java Concurrency in Practice。对于必须用 Java 编写任何严肃的并发软件的任何人来说,这本书都是必读的!

      当然,说“我从未听说过volatile,而且我的程序已经运行多年”是愚蠢的argument from ignorance

      【讨论】:

      • 这甚至无法回答我的问题。
      • 但是,如果您正在使用 Java 进行并发编程,强烈推荐这本书。
      • 当然,这是一本很棒的书,我已经读过了。我想知道的是,是否有可能真正演示这样的问题。这可以帮助说服不阅读有关 Java 并发性书籍的人。可能我的问题应该更准确。
      【解决方案6】:
      public class NoVisibility {
      
          private static boolean ready = false;
          private static int number;
      
          private static class ReaderThread extends Thread {
      
              @Override
              public void run() {
                  while (!ready) {
                      Thread.yield();
                  }
                  System.out.println(number);
              }
          }
      
          public static void main(String[] args) throws InterruptedException {
              new ReaderThread().start();
              number = 42;
              Thread.sleep(20000);
              ready = true;
          }
      }
      

      在 20 秒内调用 Thread.sleep() 会发生什么,JIT 将在这 20 秒内启动,它将优化检查并缓存值或完全删除条件。所以代码会在可见性上失败。

      要阻止这种情况发生,您必须使用 volatile

      【讨论】:

        【解决方案7】:

        @David 代码的扩展(配置 Java6 64 位,Eclipse Juno SR2):

        public class NoVisibility_Demonstration extends Thread {
            boolean keepRunning = true;
            public static void main(String[] args) throws InterruptedException {
                NoVisibility_Demonstration t = new NoVisibility_Demonstration();
                t.start();
                Thread.sleep(1000);
                t.keepRunning = false;
                System.out.println(System.currentTimeMillis() + ": keepRunning is false");
            }
            public void run() {
                int x = 10;
                while (keepRunning) 
                {
                    //System.out.println("If you uncomment this line, the code will work without the visibility issue");
                    x++;
        
                }
                System.out.println("x:"+x);
            }
        }
        

        使用此示例,您可以展示这两种情况。当您在 run() 中取消注释 while 循环中的行时,可见性问题得到解决。原因是 println() 语句使用同步。详细讨论HERE

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-12-08
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多