如何演示java多线程可见性问题?

时间:2010-05-07 08:08:13

标签: java multithreading visibility

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

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

所以我的问题是:

有人可以提供示例Java程序/代码段,可靠地显示数据可见性问题。

我认为运行一个程序并看到意外的NPE或陈旧的变量值,除了理论上的解释之外,还有助于更多的证明。

非常感谢你的帮助!

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

7 个答案:

答案 0 :(得分:21)

通过删除操作修改示例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可以优化掉内存中的读取并仅使用寄存器,甚至是处理器可以重新排序指令 - 这不会影响这种情况,但在其他多线程情况下,如果修改了多个变量,它可能会影响其他线程的感知状态。

答案 1 :(得分:7)

  

有人可以提供示例Java程序/代码段,可靠地显示数据可见性问题。

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

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

答案 2 :(得分:5)

最常见的例子强调使用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组合上公开它。

答案 3 :(得分:1)

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

当然说“我从来没有听说过volatile我的节目已经工作多年了”是一个愚蠢的argument from ignorance

答案 4 :(得分:1)

我有一段代码供您使用:

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的倍数,这是不太可能发生的。我的解释是,存在一个可见性问题导致线程仍然读取完成== false,尽管它已经更新为true。在使用“仍然工作#”语句调用同步的System.out.println()方法之后,线程读取已完成的更新值并退出。

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

答案 5 :(得分:0)

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;
    }
}

Thread.sleep()调用放置20秒,JIT将在这20秒内启动,它将优化检查并缓存值或完全删除条件。因此代码将失去可见性。

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

答案 6 :(得分:0)

@ 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