2012-01-22 20 views
9

“Java Concurrency in Practice”给出了以下不安全类的示例,这些类由于java内存模型的性质可能会永久运行或打印为0.Java内存模型同步:如何诱发数据可见性的bug?

此类尝试演示的问题是变量线程之间不会“共享”。因此,线程看到的值可能与另一个线程不同,因为它们不是易失性的或同步的。另外,由于JVM ready = true允许的语句的重新排序可能会在number = 42之前设置。

对于我来说,这个类总是使用JVM 1.6工作正常。关于如何让这个类执行不正确的行为(即打印0或永远运行)的任何想法?

public class NoVisibility { 
    private static boolean ready; 
    private static int number; 

    private static class ReaderThread extends Thread { 
     public void run() { 
      while (!ready) 
       Thread.yield(); 
      System.out.println(number); 
     } 
    } 

    public static void main(String[] args) { 
     new ReaderThread().start(); 
     number = 42; 
     ready = true; 
    } 
} 
+4

简短的回答没有。但是你真的需要检查在有人告诉你之后从屋顶跳下是否危险,并给出了充分的理由吗? – Voo

+1

使用2芯电脑。 –

+1

@Voo ...你到底在说什么? –

回答

6

您遇到的问题是,您没有足够长的时间来优化代码并缓存值。

当x86_64系统上的线程第一次读取值时,它将获得线程安全副本。它只能在以后的变化中看不到。其他CPU可能不是这种情况。

如果您尝试此操作,则可以看到每个线程都卡住了其本地值。

public class RequiresVolatileMain { 
    static volatile boolean value; 

    public static void main(String... args) { 
     new Thread(new MyRunnable(true), "Sets true").start(); 
     new Thread(new MyRunnable(false), "Sets false").start(); 
    } 

    private static class MyRunnable implements Runnable { 
     private final boolean target; 

     private MyRunnable(boolean target) { 
      this.target = target; 
     } 

     @Override 
     public void run() { 
      int count = 0; 
      boolean logged = false; 
      while (true) { 
       if (value != target) { 
        value = target; 
        count = 0; 
        if (!logged) 
         System.out.println(Thread.currentThread().getName() + ": reset value=" + value); 
       } else if (++count % 1000000000 == 0) { 
        System.out.println(Thread.currentThread().getName() + ": value=" + value + " target=" + target); 
        logged = true; 
       } 
      } 
     } 
    } 
} 

打印下面显示其fliling值,但卡住了。

Sets true: reset value=true 
Sets false: reset value=false 
... 
Sets true: reset value=true 
Sets false: reset value=false 
Sets true: value=false target=true 
Sets false: value=true target=false 
.... 
Sets true: value=false target=true 
Sets false: value=true target=false 

如果我添加-XX:+PrintCompilation此开关发生了时间,你看到

1705 1 % RequiresVolatileMain$MyRunnable::run @ -2 (129 bytes) made not entrant 
1705 2 % RequiresVolatileMain$MyRunnable::run @ 4 (129 bytes) 

这表明代码已经被编译为本地是不是线程安全的方式。

,如果你让volatile你看到它不断地翻转值的值(或直到我厌倦)

编辑:这是什么做的测试是;当它检测到的值不是那个线程目标值时,它设置该值。即。线程0设置为true,线程1设置为false当两个线程正确共享字段时,他们会看到对方发生更改,并且值不断在true和false之间翻转。

没有volatile会失败,每个线程只能看到它自己的值,所以它们都改变了值,线程0看到true,线程1看到false为同一个字段。

+1

您能否提供一个可以证明问题的循环示例? –

+0

不知道我明白了! –

+0

我已经添加了解释。让我知道你是否有更多的疑问/问题。 –

1

根据您的操作系统,Thread.yield()可能会也可能不会。 Thread.yield()不能真正被认为是平台独立的,如果你需要这个假设,就不应该使用Thread.yield()。

让示例做你期望的事情,我认为这更多的是处理器体系结构,而不是其他任何事情......尝试在不同的机器上运行它,使用不同的操作系统,看看你能从中得到什么。

+0

这与thread.yield有关......它关于java中的共享内存模型。 –

2

不是100%肯定这一点,但this可能涉及:

什么是重新排序意味着什么?

有许多箱子在其访问可 出现在一个不同的顺序是由 程序指定要执行到程序变量 (对象的实例字段,类静态字段,和数组元素)。编译器可以自由地以最优化的名义排序 指令。在某些情况下,处理器可能会执行 指令。数据可能是 在 寄存器,处理器高速缓存和主存储器之间移动的顺序与程序指定的顺序不同。

例如,如果一个线程写入字段,然后到现场b和 b的值不依赖于a的值,那么编译器是 自由地重新排列这些操作,并且缓存免费刷新b到 之前的主内存。有许多潜在的重新排序源,例如编译器,JIT和缓存。

编译器,运行时和硬件都应该合谋创建 作为-如果串行语义,这意味着在一个 单线程程序,该程序不应该能够观察 效果的幻觉的重新订购。但是,重排序可能会在 错误同步的多线程程序中发挥作用,其中一个线程是 能够观察其他线程的影响,并且可能能够 检测到变量访问变得对其他线程可见 不同于在程序中执行或指定。

+0

+1打印输出0是由于重新排序。从这本书本身来看:“NoVisibility可能会打印为零,因为写入就绪可能会在写入数字之前对读者线程可见,这种现象称为重新排序” – zengr

6

java内存模型定义了什么是工作所需要的,什么不是。不安全的多线程代码的“美”是在大多数情况下(尤其是在控制开发环境中)它通常起作用。只有当你用更好的计算机进行生产并且负载增加时,JIT才会真正开始发现bug。

+0

“并非我相信的答案。 – zengr

+1

不是一个肯定的答案,而是一个粗糙的现实! +1 – mawia

2

我认为关于这一点的要点是不能保证所有的jvms都会以相同的方式重新排序指令。它用作存在不同可能重排序的示例,因此对于jvm的某些实现,您可能会得到不同的结果。恰巧你每次都以同样的方式对jvm进行重新排序,但是对于另一个可能不是这样。保证排序的唯一方法是使用适当的同步。

0

请参阅下面的代码,它介绍了x86上的数据可见性错误。 试图与jdk8和JDK7

package com.snippets; 


public class SharedVariable { 

    private static int sharedVariable = 0;// declare as volatile to make it work 
    public static void main(String[] args) throws InterruptedException { 

     new Thread(new Runnable() { 

      @Override 
      public void run() { 
       try { 
        Thread.sleep(1000); 
       } catch (InterruptedException e) { 
        e.printStackTrace(); 
       } 
       sharedVariable = 1; 
      } 
     }).start(); 

     for(int i=0;i<1000;i++) { 
      for(;;) { 
       if(sharedVariable == 1) { 
        break; 
       } 
      } 
     } 
     System.out.println("Value of SharedVariable : " + sharedVariable); 
    } 

} 

诀窍是不要指望处理器做了重新排序,而让 编译器做了一些优化,其引入了可视性缺陷。

如果你运行上面的代码,你会看到它无限期地挂起,因为它从来没有看到更新的值sharedVariable。

要更正代码,将sharedVariable声明为volatile。

为什么正常变量不起作用,上述程序挂起?

  1. sharedVariable未被声明为volatile。
  2. 现在因为sharedVariable没有被声明为易失性编译器优化了代码。 它看到sharedVariable不会改变,所以为什么我应该在循环中每次从内存中读取 。它会将共享变量从循环中取出。类似于下面的东西。

˚F

for(int i=0;i<1000;i++)/**compiler reorders sharedVariable 
as it is not declared as volatile 
and takes out the if condition out of the loop 
which is valid as compiler figures out that it not gonna 
change sharedVariable is not going change **/ 
    if(sharedVariable != 1) { 
    for(;;) {} 
    }  
} 

在github上共享:https://github.com/lazysun/concurrency/blob/master/Concurrency/src/com/snippets/SharedVariable.java