【问题标题】:Why is this code not going into an infinite loop as suggested by JSR133?为什么这段代码没有按照 JSR133 的建议进入无限循环?
【发布时间】:2019-08-14 19:09:50
【问题描述】:

JSR-133 section 3.1 中,讨论了线程之间操作的可见性 - 提到下面的代码示例,它没有为布尔字段使用 volatile 关键字,如果两个线程正在运行它,它可能会变成一个无限循环。这是来自 JSR 的代码:

class LoopMayNeverEnd {
    boolean done = false;
    void work() {
        while (!done) {
            // do work
        }
    }
    void stopWork() {
        done = true;
    }
}

这是我感兴趣的部分中重要部分的引述:

... 现在想象一下,创建了两个线程,其中一个 线程调用work(),在某个时候,另一个线程调用stopWork()。因为有 两个线程之间没有发生之前的关系,循环中的线程可能永远不会 查看其他线程执行的更新完成...

这是我自己编写的 Java 代码,以便我可以看到它的循环:

public class VolatileTest {
    private boolean done = false;
    public static void main(String[] args) {
        VolatileTest volatileTest = new VolatileTest();
        volatileTest.runTest();
    }
    private void runTest() {
        Thread t1 = new Thread(() -> work());
        Thread t2 = new Thread(() -> stopWork());
        t1.start();
        t2.start();
    }
    private void stopWork() {
        done = true;
        System.out.println("stopped work");
    }
    private void work() {
        while(!done){
            System.out.println("started work");
        }
    }
}

尽管连续执行的结果不同 - 正如预期的那样 - 我看不到它会进入无限循环。我试图了解如何模拟文档建议的无限循环,我错过了什么?声明boolean volatile,如何去掉无限循环?

【问题讨论】:

  • 删除您的 println 语句,您可能会拥有一个永不停止的程序。但即使你不这样做,请记住,仅仅因为某件事可能发生,即使百万分之一次,并不意味着它会一直发生。它可能取决于您的 JVM、您的操作系统等。您只是无法保证线程会看到新的布尔值。这与说“保证线程不会看到它”有很大不同。
  • 感谢您的评论。删除 while 循环中的 print 语句足以使其进入无限循环......现在程序打印“停止工作”然后永远不会结束。对布尔变量 true 的赋值到哪里去了?
  • 它去了寄存器,或者是停止线程使用的CPU核心的内存缓存,甚至是主内存。但是读取线程可以继续从自己的 CPU 核心内存缓存中读取,因为该字段没有被标记为 volatile。
  • 这是有道理的。最后一个问题,将布尔值声明为 volatile 是否意味着 stopWork() 由于发生前的关系而总是首先被调用?
  • 不,一点也不。它保证如果一个线程向 volatile 字段写入了一个值,那么另一个读取 volatile 字段的值的线程将看到写入的值,而不是之前的某个值。

标签: java multithreading concurrency volatile java-memory-model


【解决方案1】:

所有 Java 值的默认、非易失性、隐式声明允许 Jit 编译器在循环外“提升”对非易失性值的引用,以便它们只被读取“一次”。在执行路径的跟踪可以安全地到达这样一个事实之后,这是允许的,即在这样一个循环内调用的方法,并不是每个都会导致返回到可能会改变这些非易失性值的值的类方法中。

最终的问题是,这种引用提升取决于值突变的“可达性”。因此,在开发过程中,您可能会遇到无法更改值的情况,因此会发生提升,突然您无法退出循环。稍后对循环的更改可能会使用一些函数,使得无法辨别循环中的逻辑无法写入该值,并且提升器消失并且循环再次工作。

关于这个问题的 java 并发邮件列表有一个最近/当前的讨论。他们似乎不认为这对 Java 开发人员来说是个问题,而且这种引用的“优化”对性能的价值远大于对开发的问题。

【讨论】:

    【解决方案2】:

    实际行为是特定于操作系统和 JVM 的。例如,默认情况下,Java 在 32 位 Windows 上以客户端模式运行,在 Mac 上以服务器模式运行。在客户端模式work 方法将终止,但不会在服务器模式 中终止。

    这是由于 Java 服务器 JIT 编译器优化造成的。 JIT 编译器可能会优化 while 循环,因为它看不到变量 done 在线程上下文中发生变化。无限循环的另一个原因可能是因为一个线程最终可能会从其寄存器或缓存中读取标志的值,而不是进入内存。因此,它可能永远不会看到另一个线程对此标志所做的更改。

    基本上通过添加volatile,您可以使拥有done 标志的线程不缓存该标志。因此,boolean 值存储在公共内存中,因此保证了可见性。此外,通过使用volatile,您可以禁用可以内联标志值的 JIT 优化。

    基本上,如果您想重现无限循环 - 只需在服务器模式下运行您的程序:

    java -server VolatileTest
    

    【讨论】:

    • 添加 -server 作为 vm 参数并没有创建无限循环
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-09-18
    • 2014-02-13
    • 2022-08-19
    • 2012-04-19
    • 1970-01-01
    • 2013-08-10
    • 2017-02-21
    相关资源
    最近更新 更多