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

Java内存模型同步:如何引发数据可见性bug?,java,concurrency,synchronization,java-memory-model,Java,Concurrency,Synchronization,Java Memory Model,“实践中的Java并发”给出了一个不安全类的示例,由于Java内存模型的性质,该类可能永远运行或打印0 这个类试图证明的问题是,这里的变量在线程之间不是“共享”的。因此,线程sees上的值可能与另一个线程不同,因为它们不是易失性的或同步的。另外,由于JVM ready=true允许对语句进行重新排序,因此可能会在number=42之前设置 对我来说,这个类在使用JVM1.6时总是工作得很好。你知道如何让这个类执行不正确的行为(例如打印0或永远运行)吗 根据您的操作系统,Thread.yield(

“实践中的Java并发”给出了一个不安全类的示例,由于Java内存模型的性质,该类可能永远运行或打印0

这个类试图证明的问题是,这里的变量在线程之间不是“共享”的。因此,线程sees上的值可能与另一个线程不同,因为它们不是易失性的或同步的。另外,由于JVM ready=true允许对语句进行重新排序,因此可能会在number=42之前设置

对我来说,这个类在使用JVM1.6时总是工作得很好。你知道如何让这个类执行不正确的行为(例如打印0或永远运行)吗


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


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

对此不是100%确定,但可能是相关的:

什么是重新排序

在许多情况下,访问程序变量 (对象实例字段、类静态字段和数组元素)可以 以与指定的不同的顺序执行 节目。编译器可以自由地对命令进行排序 以优化为名的指令。处理器可以执行 在某些情况下,指示不正常。数据可能是 在内存中的寄存器、处理器缓存和主内存之间移动 与程序指定的顺序不同

例如,如果线程先写入字段a,然后写入字段b,则 b的值不依赖于a的值,那么编译器 可以自由地对这些操作重新排序,缓存可以自由地将b刷新到 a之前的主存储器。有许多潜在的污染源 重新排序,例如编译器、JIT和缓存

编译器、运行时和硬件应该共同创建 似乎是串行语义的错觉,这意味着 单线程程序,程序应该不能观察到 重新排序的影响。然而,重新排序可以在 错误同步的多线程程序,其中一个线程 能够观察其他线程的效果,并且可能能够 检测变量访问对中的其他线程可见 与程序中执行或指定的顺序不同


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

你的问题是,你没有等待足够长的时间来优化代码和缓存值

当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:+printcomployment
这个开关大约在您看到的时候发生

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会看到同一字段的值,线程1会看到同一字段的值。

我认为主要的一点是,不能保证所有JVM都会以相同的方式重新排列指令。例如,可能存在不同的重新排序,因此对于jvm的某些实现,可能会得到不同的结果。碰巧jvm每次都以相同的方式重新排序,但另一次可能不是这样。保证订购的唯一方法是使用正确的同步。

请参阅下面的代码,它在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);
    }

}
package.com.snippets;
公共类共享变量{
private static int sharedVariable=0;//声明为volatile以使其工作
公共静态void main(字符串[]args)引发InterruptedException{
新线程(newrunnable()){
@凌驾
公开募捐{
试一试{
睡眠(1000);
}捕捉(中断异常e){
e、 printStackTrace();
}
sharedVariable=1;
}
}).start();

对于(int i=0;iShort答案是否定的。但在有人告诉你并给出了充分的理由后,你真的需要检查从屋顶上跳下来是否危险吗?@Voo…你到底在说什么?@JBNizet…现在有人使用没有多核的计算机吗?我喜欢这个问题,并找出你的程序为什么会这样运行有趣。@JBNizet-不,至少不适用于永远运行的情况。只有在单核计算机上才可能使用这种方法(如果
ReaderThread
在assig之前处于活动状态)
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);
    }

}
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(;;) {}  
    }      
}