java—为什么当涉及的线程之一是main()线程时,线程间可见性不需要volatile关键字?

7ajki6be  于 2021-08-20  发布在  Java
关注(0)|答案(2)|浏览(302)

考虑下面的程序:

import java.util.concurrent.TimeUnit;
public class StopThread {
    public static boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
        Runnable task = new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!stopRequested) {
                    i++;
                    System.out.println(i);
                }
                System.out.println("Stopping the thread!!");
            }
        };
        Thread backgroundThread = new Thread(task);

        backgroundThread.start();
        TimeUnit.SECONDS.sleep(5);
        stopRequested = true;
    }
}

这是 stopRequested 未声明为volatile-因此理想情况下线程 backgroupdThread 绝不能停止——并无休止地执行
但是在本地运行时,线程 backgroundThread 正在优雅地关闭,并显示消息:“停止线程!!”。
main()线程对共享变量的所有更新 stopRequested 其他线程可见吗?即使不使用 volatile 关键词?

juud5qan

juud5qan1#

java语言规范不能保证这一结果。
在没有同步操作的情况下(例如 volatile 写入(后续读取),写入不会在读取之前发生,因此不能保证可见。
也就是说,读取可以看到旧值,也可以看到新值;java内存模型允许任何一种结果。
要查看间隙有多窄,请尝试从循环中移除打印:

while (!stopRequested) {
                    i++;
                }

执行

openjdk version "14" 2020-03-17
OpenJDK Runtime Environment (build 14+36-1461)
OpenJDK 64-Bit Server VM (build 14+36-1461, mixed mode, sharing)

此代码不会终止。显著的区别是循环体变得不那么复杂,导致jit应用额外的优化:-)
正如您所看到的,不正确同步的程序的行为是不可预测的,只要稍有挑衅,就会发生变化。如果您想要编写健壮的多线程代码,那么您应该证明您的代码在规范方面是正确的,而不是依赖于测试。

jyztefdp

jyztefdp2#

您还没有完全了解java内存模型(jmm)的概念。
jmm基于所谓的“发生在之前/发生在之后”关系工作。这就是它的工作原理:
每当任何线程读取字段时,我们称之为“观察”。^1
如果第x行(在之前发生)和第y行(在之后发生)之间存在“在之前发生/在之后发生”关系,则jvm保证您不能像在运行x之前那样从y观察字段的值。这是整个jmm为您提供的唯一保证。它以任何其他方式提供零保证:它没有说明y的写入对x做了什么(特别是,x也可能看到y的写入,这很奇怪,因为y不是在后面运行吗?-而且,当没有hb/ha时,它也不能保证:那么y可以看到x之前的状态,或者x之后的状态,任何一个都可以发生!)
hb/ha与实际时间完全无关。如果使用时钟确定行b发生在行a之后,则不能保证这两行之间存在hb/ha关系,因此,由a导致的对字段的任何写入都不一定在b中可见。类似地,如果行b和a之间确实存在hb/ha关系,则可以保证您无法观察a从b运行之前状态中的任何字段,但实际上无法保证b在a之后实际运行(如在中,根据时钟)。通常,它必须让b观察a所做的更改,但是如果b实际上没有检查a所写的任何内容,那么jvm和cpu就没有必要小心,这两条语句可以并行运行,或者b甚至可以在a、hb/ha关系被破坏之前运行。
担保不是双向的!是的,如果b'发生在a之后,那么您可以保证不能像a之前那样观察字段的状态。但事实恰恰相反!如果a和b根本没有hb/ha关系,你就得不到任何保证。你得到的是我喜欢称之为邪恶硬币的东西。
每当没有hb/ha关系并且jvm为您读取一个字段时,jvm就会从它的厄运袋中取出邪恶的硬币并将其翻转。tails,您将获得写入之前的状态(例如,您将获得本地缓存副本)。头,你得到的是同步版本。
硬币是邪恶的,因为它不会以任意50/50的概率正面/反面落下。不。这将使您的代码在今天每次运行时都能运行良好,并且在整个星期内每次在ci服务器上运行测试套件时都能运行良好。然后在它进入生产服务器后,它仍然会以您希望的方式每次着陆。但是从现在起两周后,就在你给潜在的新客户演示的时候?
然后它决定可靠地反过来攻击你。
jmm赋予JVM这种能力。这看起来非常疯狂,但这样做是为了让jvm尽可能多地优化。任何进一步的保证都会大大降低jvm的实际运行速度。
因此,您不能让jvm掷硬币。每当您通过字段写入的方式在两个线程之间通信时,就建立hb/ha(请记住,几乎所有内容最终都是字段写入!)
如何确定hb/ha?很多,很多方法-你可以搜索它。最明显的是:
自然的hb/ha:任何一行“低于”另一行并且在同一线程中运行,都会建立hb/ha。即 x = 5; y = x; ,就像在一个线程中那样?那个 x 阅读显然会看到你的写作。
线程启动。 thread.start(); 该线程内第一行之前的hbs。 synchronized . 跳出 synchronized(x){} 在另一个线程的同步块中的第一行代码进入块(在同一个线程上)之前,块被保证为hb x !) volatile access同样建立了hb/ha,尽管很难确定哪一条线实际排在第一位(它基本上说一条em hb排在另一条之前,但是哪一条?),所以请记住这一点。
您的代码根本没有hb/ha,因此jvm正在抛出邪恶的硬币。你不能在这里得出任何结论。那个 stopRequested 可能会立即更新,可能会在下周更新,可能永远不会更新,或者可能会在5秒钟内更新。在决定这4个选项中的哪一个之前检查月亮相位的jvm是100%有效的java实现。唯一的解决方案是不要让vm掷硬币,因此,用一些东西建立hb/ha。
[1] 计算机试图并行化并应用各种完全奇怪的优化。这会影响计时(事情需要多长时间),并且没有(简单的)方法为您提供计时保证,因此jmm根本不这样做。换句话说,如果你开始计时和/或尝试注册cpu定时器,你可以“看到”各种各样的奇怪现象( System.nanoTime )并尝试记录事情的顺序,但如果您确实尝试了,请注意,几乎所有记录事件的方法都会导致同步,因此测试完全无效。关键是,如果你做对了,你可以观察到事情并行运行,甚至完全无序。保证不是关于这些,保证只是关于读取字段。

相关问题