JVM 如何在内部处理竞争条件?

Posted

技术标签:

【中文标题】JVM 如何在内部处理竞争条件?【英文标题】:How does the JVM internally handle race conditions? 【发布时间】:2015-01-27 08:29:37 【问题描述】:

如果多个线程尝试更新同一个成员变量,则称为竞争条件。但是我更感兴趣的是如果我们不在代码中通过使其同步或其他方式来处理它,那么 JVM 是如何在内部处理它的?它会挂起我的程序吗? JVM 将如何应对它?我认为 JVM 会为这种情况临时创建一个同步块,但我不确定到底会发生什么。

如果你们中的任何人有一些见解,很高兴知道。

【问题讨论】:

这是一个很棒的问题,因为 Java 是少数具有真正线程支持的 VM 的“解释型”语言之一。 Java 线程真正使用本机线程执行,其中 Python 等语言支持伪线程(我在看你,GIL)。 我找到了一些讨论here,等着看SO有什么深入的说法 它没有。无论哪个线程先运行(这取决于很多参数,包括外部参数)都会赢得比赛。结果通常是不可预测的。 JVM 本身甚至不知道存在数据竞争。 看一下没有竞争条件的 clojure 事务内存模块。 JVM 无论如何都不会处理竞争条件。如果您的代码中有竞争条件,那么您的程序就有错误,您必须自己修复它。 JVM 不会自动为您解决这个问题。 【参考方案1】:

准确的术语是数据竞争,它是竞争条件的一般概念的特化。 数据竞赛这个术语是一个官方的、精确指定的概念,这意味着它来自对代码的正式分析。

了解真实情况的唯一方法是去研究 Java 语言规范的内存模型章节,但这是一个简化的视图:无论何时发生数据竞赛,几乎无法保证结果和读取线程可能会看到已写入变量的任何值。这也是唯一的保证:线程将不会观察到从未写入过的“凭空出现”的值。好吧,除非您正在处理 longs 或 doubles,否则您可能会看到撕裂的写入。

【讨论】:

我试图在 java lang 规范中找到一个确切的 anwser Marko,但无法得到确切的答案。结果不能保证完全没问题,但我不希望出现一些不友好的行为,比如系统没有响应,因为没有一个线程可以访问更新它! 你说的是死锁吗?如果没有锁定(例如synchronized 块),它们不可能出现。 肯定不会是死锁,因为没有等待循环,只是JVM不知道该怎么做,因为JVM同时有两个请求!我认为在允许其他线程更新之前需要等待另一个完成。 不,它们可以同时更新相同的值。请注意,这假设两个线程都在各自的核心上运行。每个核心都有自己的一级缓存,可以在那里更新值。在 Intel CPU 的情况下,它将保证有一个明确的写入顺序,正如其他线程/内核所观察到的那样。 JVM 不必处理这个问题。其他 CPU(如 ARM)提供的保证较少,但仍然很容易遵守 Java 内存模型。 如果你有一个 volatile 变量,那么你就没有数据竞争,你的问题就是关于这个。当您说“堆”时,您可能指的是主要的 DRAM 内存库。不,在 Intel 上,您不必访问主 DRAM 即可保持与volatile 的兼容,因为 Intel 保证 CPU 缓存的一致性。在其他 CPU 上,您只需要发出适当的 memory barrier 指令,然后该指令将处理一致性。如果你对这些细节感兴趣,我可以在这里推荐你:gee.cs.oswego.edu/dl/jmm/cookbook.html【参考方案2】:

也许我遗漏了一些东西,但有什么需要处理的?仍然有一个线程会首先到达那里。根据哪个线程,该线程只会更新/读取一些变量并继续执行下一条指令。它不能神奇地构造一个同步块,它并不真正知道你想做什么。所以换句话说,会发生什么将取决于“比赛”的结果。

请注意,我对较低级别的东西并不感兴趣,所以也许我不完全理解您的问题的深度。

【讨论】:

如果有一些时间差没关系,但是如果 JVM 已经在更新成员对象的过程中并且在它完成之前另一个线程正在尝试再次更新它。这可能发生在当前的多线程模型中。理想情况下,JVM 应该先等待完成,然后就是临时同步块! in the process of updating the member object。这就是重点。一些操作是原子的(i = 1),所有其他操作都可以被另一个线程中断,但是 JVM 无法知道这不是您的代码想要的效果。 (另外,这就是 synchronized 的用途) 谢谢,期待这个问题的其他答案,但我喜欢原始问题 cmets 中的 mprabhat 链接。【参考方案3】:

Java 提供了synchronizedvolatile 来处理这些情况。正确使用它们可能非常困难,但请记住,Java 只是暴露了现代 CPU 和内存架构的复杂性。替代方案是始终谨慎行事,有效地同步所有会影响性能的东西;或忽略该问题并且不提供任何线程安全性。幸运的是,Java 在 java.util.concurrent 包中提供了出色的高级构造,因此您通常可以避免处理低级的东西。

【讨论】:

我认为这并不是问题的真正答案。这是关于你自己不使用同步概念时的效果。 是的,Kevin,使用 concurrentHashMap 或类似的东西来避免这些情况总是好的,这是这种情况下的最佳解决方案,但我更想知道如果我们使用基本的集合 API 会发生什么!【参考方案4】:

简而言之,JVM 假定代码在翻译成机器码时没有数据竞争。也就是说,如果代码未正确同步,Java 语言规范仅提供关于该代码行为的有限保证。

大多数现代硬件同样假定代码在执行时没有数据竞争。也就是说,如果代码没有正确同步,硬件对其执行结果只能做出有限的保证。

特别是 Java 语言规范 guarantees 以下仅在没有数据竞争的情况下:

可见性:读取一个字段会产生最后分配给它的值(不清楚哪个写入是last,写入长变量或双变量need not be atomic)

ordering:如果写入可见,那么在它之前的任何写入也是可见的。例如,如果一个线程执行:

x = new FancyObject();

只有在FancyObject的构造函数完全执行后,另一个线程才能读取x

在存在数据竞争的情况下,这些保证无效。读取线程可能永远不会看到写入。也可以看到x 的写入,而没有看到逻辑上在x 写入之前的构造函数的效果。如果不能做出这样的基本假设,那么程序就不太可能是正确的。

然而,数据竞争不会损害 Java 虚拟机的完整性。特别是 JVM 不会崩溃或停止,仍然保证内存安全(即防止内存损坏)和certain semantics of final fields。

【讨论】:

【参考方案5】:

JVM 会很好地处理这种情况(即它不会挂起或抱怨),但你可能得不到你喜欢的结果!

当涉及多个线程时,java 变得异常复杂,甚至看起来明显正确的代码也可能被严重破坏。举个例子:

public class IntCounter 
    private int i;

    public IntCounter(int i)
         this.i = i;
    

    public void incrementInt()
        i++;
    

    public int getInt()
        return i;
    

在很多方面存在缺陷。

首先,假设 i 当前为 0,线程 A 和线程 B 几乎同时调用 incrementInt()。存在一种危险,即他们都会看到 i 为 0,然后都将其递增 1,然后保存结果。所以在两次调用结束时,i 只是 1,而不是 2!

这是代码的竞争条件问题,但还有其他关于内存可见性的问题。当线程 A 更改共享变量时,无法保证(没有同步)线程 B 会看到更改!

因此线程 A 可以将 i 增加 100 次,一个小时后,调用 getInt() 的线程 B 可能会将 i 视为 0、100 或介于两者之间的任何值!

如果您正在研究 Java 并发,唯一明智的做法是阅读 Brian Goetz 等人的 Java Concurrency in Practice。 (好吧,可能还有其他了解它的好方法,但这是一本由 Joshua Bloch、Doug Lea 和其他人合着的好书)

【讨论】:

天哪,你是第10个人左右,连这个问题都不懂…… OP 的问题是“它会挂起我的程序吗,JVM 会如何反应它?”我回答“它不会挂起或抱怨”,在我看来,这似乎令人满意地回答了他的问题。由于他的问题表明他对 Java 并发整体不熟悉,所以我给了他一些我认为可以帮助他避免严重错误的额外信息。我喜欢尝试具有建设性。我很想知道你为什么认为我不理解他的问题,或者你认为他的问题是什么。 @krmanish007 没有 JVM。只有 Java 语言规范 (JLS)。 A JVM(不是 the JVM)是​​大多数 JLS 实现的运行时部分的一个组件。如果任何特定的 JVM 未能按照 JLS 所说的去做,那么就是 JVM 出了问题。 JLS是权威。 JLS 并不容易阅读,但它确实说明了当两个不同的线程在没有同步的情况下访问同一个变量时可能会发生什么以及可能不会发生什么。

以上是关于JVM 如何在内部处理竞争条件?的主要内容,如果未能解决你的问题,请参考以下文章

控制台窗口如何在内部处理 JavaScript 算术赋值?返回值从哪里来?

我的 C# win 表单应用程序在内部调用批处理脚本,如何在 Visual Studio 中导出时对最终用户隐藏它?

spring/hibernate 如何为我们提供防止 SQL 注入的保证以及它是如何在内部处理的?

GCM 之类的推送通知如何在内部工作?

带有肤色的表情符号如何在内部表示? [关闭]

mllib 如何在内部对不平衡数据集的类进行加权?