9

“实践中的 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;
    }
}
4

6 回答 6

6

Java 内存模型定义了工作所需的内容和不工作的内容。不安全的多线程代码的“优点”是在大多数情况下(尤其是在受控的开发环境中)它通常可以工作。只有当您使用更好的计算机投入生产并且负载增加并且 JIT 真正启动时,错误才会开始咬人。

于 2012-01-22T13:13:05.640 回答
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,你会看到它无休止地翻转价值(或者直到我感到无聊)

编辑:这个测试的作用是;当它检测到该值不是线程目标值时,它设置该值。IE。线程 0 设置为true,线程 1 设置为false 当两个线程正确共享字段时,它们会看到彼此发生变化,并且值不断在真假之间翻转。

如果没有 volatile,这将失败,并且每个线程只能看到自己的值,因此它们都更改值和线程 0 看到true和线程 1 看到false相同的字段。

于 2012-01-22T13:15:05.227 回答
2

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

重新排序是什么意思?

在许多情况下,访问程序变量(对象实例字段、类静态字段和数组元素)的执行顺序可能与程序指定的顺序不同。编译器可以自由地以优化的名义对指令的顺序进行随意处理。在某些情况下,处理器可能会乱序执行指令。数据可能以不同于程序指定的顺序在寄存器、处理器高速缓存和主存储器之间移动。

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

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

于 2012-01-22T13:12:07.083 回答
2

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

于 2012-01-22T13:22:04.260 回答
1

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

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

于 2012-01-22T13:09:23.023 回答
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 没有被声明为 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

于 2015-01-29T15:00:35.907 回答