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

发布于 2024-12-28 05:59:47 字数 709 浏览 2 评论 0原文

“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;
    }
}

"Java Concurrency in Practice" gives the following example of an unsafe class which due the nature of the java memory model may end up running forever or print 0.

The issue this class is trying to demonstrate is that the variables here are not "shared" between threads. So the value on thread sees can be different from another thread as they are not volatile or synchronized. Also due to the reordering of statements allowed by the JVM ready=true maybe set before number=42.

For me this class always works fine using JVM 1.6. Any idea on how to get this class to perform the incorrect behavior (i.e. print 0 or run forever)?

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;
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

迷爱 2025-01-04 05:59:47

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

当 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

的时间

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

如果我添加 -XX:+PrintCompilation 这个切换发生在您看到这表明代码已编译为本机是一种非线程安全的方式

。如果你将值设置为易失性,你会看到它无休止地翻转该值(或者直到我感到无聊)

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

如果没有 挥发性,这将失败,并且每个线程只能看到自己的值,因此它们都更改值,并且对于同一字段,线程 0 看到 true ,线程 1 看到 false

The problem you have is that you are not waiting long enough for the code to be optimised and the value to be cached.

When a thread on an x86_64 system reads a value for the first time, it gets a thread safe copy. Its only later changes it can fail to see. This may not be the case on other CPUs.

If you try this you can see that each thread is stuck with its local value.

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;
                }
            }
        }
    }
}

prints the following showing its fliping the value, but gets stuck.

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

If I add -XX:+PrintCompilation this switch happens about the time you see

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

Which suggests the code has been compiled to native is a way which is not thread safe.

if you make the value volatile you see it flipping the value endlessly (or until I got bored)

EDIT: What this test does is; when it detect the value is not that threads target value, it set the value. ie. thread 0 sets to true and thread 1 sets to false When the two threads are sharing the field properly they see each others changes and the value constantly flips between true and false.

Without volatile this fails and each thread only sees its own value, so they both both changing the value and thread 0 see true and thread 1 sees false for the same field.

银河中√捞星星 2025-01-04 05:59:47

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

The java memory model defines what is required to work and what isn't. the "beauty" of unsafe multi-threaded code is that in most situations (especially in contolled dev environments) it usually works. it's only when you get to production with a better computer and load increases and the JIT really kicks in that the bugs start to bite.

哀由 2025-01-04 05:59:47

对此不是100%确定,但是这个< /a> 可能相关:

重新排序是什么意思?

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

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

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

Not 100% sure on this, but this might be related:

What is meant by reordering?

There are a number of cases in which accesses to program variables
(object instance fields, class static fields, and array elements) may
appear to execute in a different order than was specified by the
program. The compiler is free to take liberties with the ordering of
instructions in the name of optimization. Processors may execute
instructions out of order under certain circumstances. Data may be
moved between registers, processor caches, and main memory in
different order than specified by the program.

For example, if a thread writes to field a and then to field b, and
the value of b does not depend on the value of a, then the compiler is
free to reorder these operations, and the cache is free to flush b to
main memory before a. There are a number of potential sources of
reordering, such as the compiler, the JIT, and the cache.

The compiler, runtime, and hardware are supposed to conspire to create
the illusion of as-if-serial semantics, which means that in a
single-threaded program, the program should not be able to observe the
effects of reorderings. However, reorderings can come into play in
incorrectly synchronized multithreaded programs, where one thread is
able to observe the effects of other threads, and may be able to
detect that variable accesses become visible to other threads in a
different order than executed or specified in the program
.

囚你心 2025-01-04 05:59:47

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

I think the main point about this is that it is not guaranteed that all jvms will reorder the instructions in the same way. It is used as an example that different possible reorderings exist, and therefore for some implementations of the jvm you might get different results. It just so happens that you jvm is reordering in the same way every time, but that might not be the case for another. The only way to guarantee ordering is to use proper synchronisation.

混浊又暗下来 2025-01-04 05:59:47

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

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

Depending on your OS, Thread.yield() might or might not work.
Thread.yield() cannot really be considered platform independent, and shouldn't be used if you need that assumption.

Making the example do what you expect it to do, I think that's more a matter of processor architecture than anything else... try running it on different machines, with different OS's, see what you can get out of it.

丢了幸福的猪 2025-01-04 05:59:47

请看下面的代码,它引入了 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。

要更正代码,请将共享变量声明为易失性。

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

  1. SharedVariable 未声明为 volatile。
  2. 现在,因为sharedVariable没有声明为易失性编译器优化了代码。
    它看到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

Please see the below code, It introduces the data visibility bug on x86.
Tried with jdk8 and 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 is not to expect the processor to do the reordering rather make
compiler to make some optimization which introduces the visibility bug.

If you run the above code you will see it hangs indefinitely because it never sees the updated value sharedVariable.

To correct the code declare the sharedVariable as volatile.

Why normal variable didn't work and the above program hangs ?

  1. sharedVariable was not declared as volatile.
  2. Now because sharedVariable was not declared as volatile compiler optimizes the code.
    It sees that sharedVariable is not going be changed so why i should read
    from memory every time in the loop. It will take the sharedVariable out of the loop. Something similar to below.

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

Shared at github : https://github.com/lazysun/concurrency/blob/master/Concurrency/src/com/snippets/SharedVariable.java

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文