Spe*_*ami 1 java multithreading synchronization
当我运行代码时,System.out.println(counter);有时会打印小于 10 000 的数字。在运行代码之前,我认为计数器应该在 [10 000, 20 000] 之间,具体取决于操作系统调度程序是否决定在单独的核心上或在同一核心上通过在它们之间切换或两者的组合来执行主线程和 t1 。
public class App {
private static int counter = 0;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 10_000; i++) {
counter++;
}
}
});
t1.start();
for(int i = 0; i < 10_000; i++) {
counter++;
}
System.out.println(counter); // *****
}
}
Run Code Online (Sandbox Code Playgroud)
我相信这个逻辑可以解释原因(虽然我不确定我是否正确)
我的解释可以解释为什么代码有时会打印小于 10 000 的数字吗?该程序可以打印大于 20 000 的数字吗?(因为我相信答案是肯定的,因为上面的逻辑类似(我已经执行了该程序很多次,但我还没有看到控制台打印大于 20 000 的数字)。
\n\n我的解释可以解释为什么代码有时会打印小于 10 000 的数字吗?
\n
不。
\n\n\n在这段时间内,计数器 = 5699
\n
你的思想有问题。
\n不存在“价值counter”。那不是一件事。
这就是“该领域众多克隆之一的价值”。
\n相关文档/规范是Java 内存模型 (JMM)(请参阅Java 语言规范中描述的Java 内存模型部分 (\xc2\xa717.4) )。它解释了Java 虚拟机 (JVM)实现有多种选择,并且 Java 程序员有责任编写代码,以便无论 JVM 做出什么选择,代码都能正常工作。
\nJMM中规定的具体规则涉及可观察性。它定义了以下条件:
\nJVM充分利用它在 JMM 下获得的权利。这不仅仅是关于哪个语句“在另一个语句之前运行”,甚至一个线程所做的任何更新是否同步到另一个线程。它甚至与 JVM 的完全重新排序有关。
\n该列表通常称为“发生于”列表,因为 JMM 使用的术语是“发生于”。以下是该列表中最相关的部分:
\n线程内“自然”HB:如果 A 和 B 在同一线程中运行,则 HB(A, B) 成立,并且根据执行规则,A 先于 B。这就是“duh,显然”规则。鉴于:x = 5; x = 10; y = x;JVM 重新排序是非法的x = 5;,现在是x = 10;5。y然而,这只是关于可观察性。因此,给定:x = 5; y = 10;JVM 可以自由地为x = 5; y = 10;首先设置 y 然后设置 x 生成优化的机器代码。有时它实际上会这样做,以优化 CPU 流水线。这并不意味着 JVM 总是会这样做,或者永远会这样做 - 它只是意味着如果JVM 实现这样做,那么它没有任何问题。有些人确实这样做。JVM 甚至不必保持一致;它现在可能会这样做,明天就不会了:这也不仅仅是为了扰乱你,例如,JVM 会跟踪if最近执行中分支 (an) 的走向,以及热点时的情况。机器代码,将使用它来编写分支预测循环(在机器代码中,没有 while 或 if,有(条件)GOTO(JMP)。随着 CPU“向前看”和预执行,跳转往往会更慢-处理即将到来的指令,如果跳转则毫无意义。分支预测循环或 if 是机器代码不会跳转到最常见路径的循环。如果“最近执行”略有不同,JVM 可能会生成不同的分支预测代码,并且从该分歧中,任何事情都可能发生。
Thread ops HB:HB(t.start(), run)建立在t.start()启动线程的代码所在的位置,并且run是该线程的方法中的第一行run()。类似地,HB(end, join)定义为:线程的最后一行代码发生t.join()在该线程返回之前。
synchronized:存在一种线程在同一监视器上通过同步块的顺序,即如果一个线程有synchronized (x) {}并且另一个线程也有该代码(其中 x 指向同一个对象),您可以准确列出线程进入这些块的顺序。HB(end, start)成立,其中“end”是首先经过的线程的同步块中的最后一行,“start”是最后经过的线程的同步块中的第一行。关于访问变量也存在类似的规则,但通常在涉及变量volatile时要弄清楚哪一个是“之前”、哪一个是“之后”要困难得多。volatile
还有一些。重要的是要认识到大量的java.*API 调用会建立 HB。例如,尝试通过发出一堆System.out.println语句来见证这些东西的运行只会让你感到困惑,因为这会建立各种 HB,从而扰乱任何重现某些竞争条件的尝试。这东西是火箭科学。
以上是 JMM 的简化,但相当准确。在实践中这可以归结为两件事:
\ncount现在是5000。counter = 2500其中了。M获取核心并需要加载counter,并且它在 T1 写出之前进入,因此加载了 2500。紧接着,T1 将其 10000 写入。counter是 7500。它已经完成,因此将其写回内存。多田:里面不到一万。
\n警告:这个故事是虚构的。重点并不是所有 CPU 硬件都以这种方式工作。要点是:这是 [A] 一个看似合理的故事(CPU 确实以这种方式工作),并且 [B] 该故事中没有任何内容违反 JMM 规范。
\n“为什么会发生这种情况”通常不适用于 java 代码,因为 java 运行在大量的硬件上,并且具有几乎同样大量的工作方式。这就是为什么你必须回到 JMM:JVM可以做什么?您能否想出一种方法,让 JVM 执行您的代码,同时遵守 JMM 中规定的保证,但会导致您的代码不执行您想要的操作?如果答案是“是”,那么您的代码就完全损坏了,并且您无法设法找到架构、操作系统和系统负载的组合来实际导致问题并不相关:您的代码已损坏,如果你继续这样做,有一天它会失败,这将是你的错。那一天可能永远不会到来,直到它运行在全新的硬件(例如,ARM Mac,如果您测试代码的全部是英特尔芯片,则架构完全不同),甚至是新的JDK版本。
\n