Java内存模型同步:如何诱导数据可见性bug?

时间:2012-01-22 13:00:20

标签: java concurrency synchronization java-memory-model

“实践中的Java并发”给出了以下不安全类的示例,由于Java内存模型的性质可能最终会永久运行或打印为0.

这个类试图证明的问题是这里的变量不是线程之间的“共享”。因此,线程看到的值可能与另一个线程不同,因为它们不是易失性或同步的。此外,由于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;
    }
}

6 个答案:

答案 0 :(得分:6)

java内存模型定义了工作所需的内容和不需要的内容。不安全的多线程代码的“美”在大多数情况下(特别是在受控的开发环境中)通常都有效。只有当你使用更好的计算机进行生产并且负载增加时,JIT才能真正开始尝试使用这些错误。

答案 1 :(得分: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;
                }
            }
        }
    }
}

打印以下内容,显示其正在掠过该值,但卡住了。

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相同的字段。

答案 2 :(得分:2)

不是100%肯定,但this可能是相关的:

  

重新排序是什么意思?

     

在许多情况下访问程序变量   (对象实例字段,类静态字段和数组元素)可以   似乎以不同于指定的顺序执行   程序。编译器可以自由地使用自由命令   优化名称中的说明。处理器可以执行   在某些情况下,指令无序。数据可能是   在寄存器,处理器缓存和主存储器之间移动   不同于程序指定的顺序。

     

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

     

编译器,运行时和硬件应该密谋创建   as-if-serial语义的错觉,意味着在a   单线程程序,程序应该无法观察到   重新排序的影响。但是,重新排序可以发挥作用   错误同步的多线程程序,其中一个线程是   能够观察其他线程的影响,并且可能   检测变量访问对于其他线程是否可见   不同于程序中执行或指定的顺序

答案 3 :(得分:2)

我认为关于这一点的要点是,不能保证所有的jvms都会以相同的方式重新排序指令。它用作存在不同可能的重新排序的示例,因此对于jvm的某些实现,您可能会得到不同的结果。只是碰巧你jvm每次都以相同的方式重新排序,但对另一个人来说情况可能并非如此。保证排序的唯一方法是使用正确的同步。

答案 4 :(得分:1)

根据您的操作系统,Thread.yield()可能会也可能不起作用。 Thread.yield()实际上不能被视为独立于平台,如果您需要这个假设,则不应该使用它。

让示例按照您的期望执行,我认为这更像是处理器架构而不是其他任何事情......尝试在不同的机器上运行它,使用不同的操作系统,看看你可以从中获得什么。 / p>

答案 5 :(得分: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);
    }

}
  

Trick不希望处理器进行重新排序而是制作   编译器进行一些引入可见性错误的优化。

如果您运行上面的代码,您将看到它无限期挂起,因为它永远不会看到更新的值sharedVariable。

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

为什么普通变量不起作用且上述程序挂起?

  1. sharedVariable未声明为volatile。
  2. 现在因为sharedVariable未被声明为volatile编译器优化代码。 它看到sharedVariable不会被改变,所以为什么我应该阅读 每次在循环中从内存中。这将使sharedVariable脱离循环。类似于下面的东西。
  3. ˚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