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



"Java Concurrency in Practice"给出了以下不安全类的示例,由于Java内存模型的性质,该类可能最终永远运行或打印0。

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

您遇到的问题是等待代码优化和缓存值的时间不够长。

当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

java内存模型定义了哪些需要工作,哪些不需要。不安全的多线程代码的"美妙之处"在于,在大多数情况下(尤其是在受控制的开发环境中),它通常都能工作。只有当你用一台更好的计算机进行生产,负载增加,JIT真正发挥作用时,错误才会开始肆虐。

对此不能100%确定,但这可能与有关

重新排序是什么意思?

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

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

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

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

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

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

请参阅下面的代码,它介绍了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没有被声明为volatile编译器,因此优化了代码。它看到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

最新更新