JVM内部如何处理竞态条件?

24

如果多个线程尝试更新同一个成员变量,就会发生竞态条件。但我更想知道如果我们不通过使其同步或使用其他方法在代码中处理它,JVM 如何在内部处理它?它会挂起我的程序吗?JVM 会如何对其作出反应?我认为 JVM 会暂时为此情况创建一个同步块,但我不确定具体会发生什么。

如果有人有一些见解,那就好了。


4
这是一个非常棒的问题,因为Java是少数具有真正线程支持的VM的“解释型”语言之一。Java线程实际上是使用本地线程执行的,而像Python这样的语言则具有伪线程支持(我在看你,GIL)。 - vz0
我在这里找到了一些讨论(链接:http://programmers.stackexchange.com/questions/262428/race-conditions-in-jvm-languages-versus-c-c),正在等待看看Stack Overflow对此有何深入的说法。 - mprabhat
它并不关心哪个线程先运行(这取决于许多参数,包括外部因素),谁先到达终点就赢了比赛。结果通常是不可预测的。JVM本身甚至不知道存在数据竞争。 - njzk2
我不想让JVM修复它,但想知道JVM会如何反应。可能的结果是什么! - krmanish007
不要对这些甚至无法理解你问题的可怜人绝望,这是Java世界。Sun JVM是用C++编写的,在每个系统上它都使用操作系统的线程锁定机制,尽管在最新的JVM中有显着的改进来最小化竞争问题的可能性。 - peterh
显示剩余2条评论
5个回答

17

准确的术语是 数据竞争,它是 竞态条件 这一概念的一个特例。术语 数据竞争 是一个正式、明确定义的概念,这意味着它来自对代码的 形式化 分析。

获取真实情况的唯一方法是去学习 Java 语言规范中的内存模型章节,但这只是一个简化的观点:每当你有数据竞争时,几乎没有任何保证结果是什么,读取线程可能会看到变量曾经写入过的任何值。其中也包含唯一的保证:该线程将 不会 观察到“空气中出现”的值,即从未被写入的值。嗯,除非你处理的是 longdouble,那么你可能会看到断裂的写入。


2
你是在谈论死锁吗?如果没有锁定(例如synchronized块),它们不可能出现。 - Marko Topolnik
绝对不会发生死锁,因为没有等待的循环,但问题在于JVM不知道该怎么做,因为同时有两个请求!我认为在允许其他线程更新之前,需要等待另一个线程完成。 - krmanish007
1
不,它们都可以同时更新相同的值。请注意,这假定两个线程分别在自己的核心上运行。每个核心都有自己的一级缓存,并且可以在那里更新该值。在英特尔CPU的情况下,它将保证其他线程/核心观察到写入的明确顺序。JVM不必处理这个问题。其他CPU(例如ARM)提供的保证较少,但仍然很容易遵守Java内存模型。 - Marko Topolnik
1
如果您有一个易失性变量,那么您就没有数据竞争的问题了,您的问题就在于此。当您说“堆”时,您可能指的是主DRAM内存银行。而且,在英特尔上,您不必访问主DRAM即可保持符合volatile,因为英特尔保证CPU缓存一致性。在其他CPU上,您只需要发出适当的内存屏障指令,然后处理一致性即可。如果您对这些细节感兴趣,我可以向您推荐这里:http://gee.cs.oswego.edu/dl/jmm/cookbook.html - Marko Topolnik
我不是非常确定,但会去阅读一下。了解这些是很好的。 - krmanish007
显示剩余7条评论

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

请注意,我对较低层次的东西不是很熟悉,因此也许我并没有完全理解您的问题的深度。


如果存在一些时间差异,那么没关系,但是如果JVM已经在更新成员对象的过程中,在它完成之前另一个线程又试图再次更新它,这种情况可能会发生在当前的多线程模型中。理想情况下,JVM应该等待第一个完成,但那只是临时同步块! - krmanish007
正在更新成员对象。这就是重点。一些操作是原子性的(例如i = 1),其他所有操作都可能被另一个线程中断,但JVM无法知道这不是您代码预期的效果。(此外,这也是synchronized的作用)。 - njzk2
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Sebastiaan van den Broek

2
Java提供了`synchronized`和`volatile`来处理这些情况。正确使用它们可能会令人沮丧地困难,但请记住,Java只是暴露了现代CPU和内存架构的复杂性。另外的选择是总是谨慎行事,有效地同步所有内容,这将损害性能;或者忽略问题并不提供任何线程安全性。幸运的是,Java在`java.util.concurrent`包中提供了出色的高级构造,因此您通常可以避免处理低级别的东西。

我认为这并不是对问题的真正回答。它是关于当您自己不使用同步概念时的影响。 - Sebastiaan van den Broek
是的,Kevin,使用concurrentHashMap或类似的东西总是很好,以避免这些情况,这是在这种情况下最优的解决方案,但我更想知道如果我们使用基本的集合API会发生什么! - krmanish007

1
简而言之,JVM在将代码翻译为机器代码时假定代码没有数据竞争。也就是说,如果代码未正确同步,Java语言规范仅提供有关该代码行为的有限保证。
当执行代码时,大多数现代硬件同样假定代码没有数据竞争。也就是说,如果代码未正确同步,硬件仅对其执行结果提供有限保证。
特别地,在不存在数据竞争的情况下,Java语言规范仅保证以下内容:
  • visibility: reading a field yields the value last assigned to it (it is unclear which write was last, and writes of long or double variables need not be atomic)
  • ordering: if a write is visible, so are any writes preceding it. For instance, if one thread executes:

    x = new FancyObject();
    

    another thread can read x only after the constructor of FancyObject has executed completely.

在数据竞争的情况下,这些保证将无效。读线程可能永远不会看到写入。也有可能看到x的写入,而没有看到逻辑上先于x写入的构造函数的效果。如果不能做出这样基本的假设,则程序很可能不正确。
然而,数据竞争不会危及Java虚拟机的完整性。特别是,JVM不会崩溃或停止,并仍然保证内存安全(即防止内存损坏)和final字段的某些语义

0

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并发编程实践》(好吧,可能还有其他学习方法,但这是一本由Joshua Bloch、Doug Lea和其他人共同撰写的优秀书籍)。


1
哦,我的天啊,你是第十个甚至都不理解这个问题的人... - peterh
OP的问题是“它会挂起我的程序,JVM将如何反应?” 我回答说,“它不会挂起或抱怨”,这对我来说似乎已经令他满意地回答了他的问题。由于他的问题表明他对Java并发编程不太熟悉,因此我给他提供了一些额外的信息,希望能帮助他避免糟糕的错误。我喜欢尝试做出有益的贡献。我很想知道你为什么认为我没有理解他的问题,或者你认为他的问题是什么。 - user384842
@krmanish007 没有JVM。只有Java语言规范(JLS)。JVM(不是JVM)是大多数JLS实现的运行时部分的一个组件。如果任何特定的JVM未能执行JLS所说应该执行的操作,则是JVM出了问题。JLS是权威。JLS并不容易阅读,但它确实说明了当两个不同的线程在没有同步的情况下访问相同变量时可能发生什么以及可能不会发生什么。 - Solomon Slow

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接