gpt4 book ai didi

java - 解释JIT重新排序的工作方式

转载 作者:行者123 更新时间:2023-12-03 12:54:42 24 4
gpt4 key购买 nike

我已经阅读了很多有关Java同步以及可能发生的所有问题的文章。但是,我仍然有些困惑的是JIT如何重新排序写入。

例如,一个简单的双重检查锁对我来说很有意义:

  class Foo {
private volatile Helper helper = null; // 1
public Helper getHelper() { // 2
if (helper == null) { // 3
synchronized(this) { // 4
if (helper == null) // 5
helper = new Helper(); // 6
}
}
return helper;
}
}

我们在第1行使用volatile来强制发生事前关系。没有它,JIT完全有可能整理我们的代码。例如:
  • 线程1位于第6行,并且内存已分配给helper,但是,构造函数尚未运行,因为JIT可以对我们的代码进行重新排序。
  • 线程2从第2行进入,并获取一个尚未完全创建的对象。

  • 我理解这一点,但是我不完全理解JIT在重新排序方面的局限性。

    例如,假设我有一个创建 MyObject并将其放入 HashMap<String, MyObject>的方法(我知道 HashMap并非线程安全的,不应在多线程环境中使用,但请耐心等待)。线程1调用createNewObject:
    public class MyObject {
    private Double value = null;

    public MyObject(Double value) {
    this.value = value;
    }
    }

    Map<String, MyObject> map = new HashMap<String, MyObject>();

    public void createNewObject(String key, Double val){
    map.put(key, new MyObject( val ));
    }

    同时线程2从Map调用get。
    public MyObject getObject(String key){
    return map.get(key);
    }

    线程2是否有可能从 getObject(String key)接收未完全构造的对象?就像是:
  • 线程1:为new MyObject( val )分配内存
  • 线程1:将对象放置在 map 中
  • 线程2:调用getObject(String key)
  • 线程1:完成构建新的MyObject。

  • 还是 map.put(key, new MyObject( val ))不会在完全构建对象之前将其放入 map 中?

    我猜想答案是,在对象完全构建之前,它不会将对象放到Map中(因为听起来很糟糕)。那么,JIT如何重新排序?

    简而言之,它只能在创建新的 Object并将其分配给引用变量(例如经过双重检查的锁)时重新排序吗?一个完整的JIT概要对于一个SO答案来说可能很多,但是我真正好奇的是它如何重新排序一个写操作(如双重检查锁的第6行),以及阻止它将对象放入 Map的原因。尚未完全建成。

    最佳答案

    警告:文字墙
    您的问题的答案在水平线之前。我将在答案的第二部分(与JIT无关,因此仅在您对JIT感兴趣的情况下就这样)中更深入地解释基本问题。问题第二部分的答案位于底部,因为它取决于我进一步描述的内容。
    TL; DR在您通过编写线程不安全代码让它们有效的条件下,JIT可以执行任何所需的操作,JMM可以执行所需的任何操作。
    注意:“初始化”是指构造函数中发生的事情,它不包括其他任何事情,例如在构造之后调用静态init方法等。

    "If the reordering produces results consistent with a legal execution, it is not illegal." (JLS 17.4.5-200)


    如果一组操作的结果符合根据JMM的有效执行链,则无论作者是否希望代码产生该结果,都将允许该结果。

    "The memory model describes possible behaviors of a program. An implementation is free to produce any code it likes, as long as all resulting executions of a program produce a result that can be predicted by the memory model.

    This provides a great deal of freedom for the implementor to perform a myriad of code transformations, including the reordering of actions and removal of unnecessary synchronization" (JLS 17.4).


    除非我们不允许使用JMM(在多线程环境中),否则JIT将对其进行适当的排序。
    JIT可以或将要做什么的细节是不确定的。查看数百万个运行样本不会产生有意义的模式,因为重新排序是主观的,它们取决于非常具体的细节,例如CPU架构,时序,试探法,图形大小,JVM供应商,字节码大小等。我们只知道当JIT不需要遵循JMM时,它将假定代码在单线程环境中运行。最后,JIT对您的多线程代码影响很小。如果您想更深入,请参阅此 SO answer并对诸如 IR GraphsJDK HotSpot source和诸如 this one之类的编译器文章进行一些研究。但是,再次提醒您,JIT与您的多线程代码转换几乎没有关系。

    实际上,“尚未完全创建的对象”不是JIT的副作用,而是内存模型(JMM)。总而言之,JMM是一种规范,它对某些 Action 集的结果进行保证,其中 Action 是涉及共享状态的操作。诸如 atomicity, memory visibility, and ordering之类的高级概念更容易理解JMM,这三个概念是线程安全程序的组件。
    为了证明这一点,您的第一个代码示例(DCL模式)极不可能被JIT修改,从而产生“一个尚未完全创建的对象”。实际上,我相信 不可能做到这一点,因为它不会遵循单线程程序的顺序或执行。
    那么,这到底是什么问题呢?
    问题在于,如果未按同步顺序,事前发生的顺序等对操作进行排序(又由 JLS 17.4-17.5进行描述),则不能保证线程会看到执行此类操作的副作用。 线程可能不会刷新其缓存以更新该字段,线程可能会观察到写入不正确的情况。 特定于此示例,允许线程查看处于不一致状态的对象,因为该对象未正确发布。我敢肯定,即使您曾经使用多线程进行过最细微的工作,也已经听说过安全发布。
    您可能会问, 很好,如果JIT无法修改单线程执行,为什么可以是多线程版本?
    简而言之,这是因为由于缺少适当的同步,允许线程认为(如教科书中通常所写的那样“感知”)初始化是乱序的。

    "If Helper is an immutable object, such that all of the fields of Helper are final, then double-checked locking will work without having to use volatile fields. The idea is that a reference to an immutable object (such as a String or an Integer) should behave in much the same way as an int or float; reading and writing references to immutable objects are atomic" (The "Double-Checked Locking is Broken" Declaration).


    使对象不可变可确保状态为 fully initialized when the constructor exits
    请记住,对象构造始终是不同步的。相对于构造该对象的线程,正在初始化的对象是唯一可见且安全的。为了让其他线程看到初始化,您必须安全地发布它。这些方法是:

    "There are a few trivial ways to achieve safe publication:

    1. Exchange the reference through a properly locked field (JLS 17.4.5)
    2. Use static initializer to do the initializing stores (JLS 12.4)
    3. Exchange the reference via a volatile field (JLS 17.4.5), or as the consequence of this rule, via the AtomicX classes
    4. Initialize the value into a final field (JLS 17.5)."

    (Safe Publication and Safe Initialization in Java)


    安全发布确保完成后其他线程将能够看到完全初始化的对象。
    重新考虑我们的想法,即只有保证线程按顺序才能保证看到副作用,所以需要 volatile的原因是为了使线程1中对助手的写入相对于线程2中的读取是有序的。允许在读取后感知初始化,因为它发生在向辅助程序的写入之前。它背负着 volatile 写操作,以便必须在初始化之后进行读取,然后再对 volatile 字段(传递属性)进行写操作。
    总而言之,仅在创建对象之后才进行初始化,这仅是因为另一个线程按顺序进行了思考。由于JIT优化,构造后永远不会进行初始化。您可以通过在可变字段中确保适当的发布或使助手不可变来解决此问题。

    现在,我已经描述了JMM中发布如何工作的基本概念,希望很容易理解您的第二个示例将如何工作。

    I'd imagine that the answer is, it wouldn't put an object into the Map until it is fully constructed (because that sounds awful). So how can the JIT reorder?


    对于构造线程,它将在初始化后将其放入映射中。
    对于读者线程,它可以看到任何想要的东西。 (在HashMap中构造不正确的对象?这绝对在可能性范围之内)。
    您用4个步骤描述的是 完全合法的。在分配 value或将其添加到 map 之间没有顺序,因此线程2 会感知到的困惑,因为 MyObject是不安全发布的。
    您实际上可以通过仅转换为 ConcurrentHashMap来解决此问题,并且 getObject()将完全是线程安全的,因为一旦将对象放入 map 中,初始化将在put之前进行,并且由于 get被执行,因此两者都需要在 ConcurrentHashMap之前进行线程安全。但是,一旦修改了对象,这将成为管理方面的噩梦,因为您需要确保更新状态是可见的和原子的-如果一个线程检索到一个对象,而另一个线程在第一个线程可以完成修改和放置之前更新了该对象,该怎么办?它回到 map 上了吗?
    T1 -> get() MyObject=30 ------> +1 --------------> put(MyObject=31)
    T2 -------> get() MyObject=30 -------> +1 -------> put(MyObject=31)
    或者,您也可以使 MyObject不可变,但是您仍然需要映射 ConcurrentHashMap,以便其他线程查看 put-线程缓存行为可能会缓存旧副本,而不刷新并继续重用旧版本。 ConcurrentHashMap确保其写入对读者可见,并确保线程安全。回顾线程安全的三个先决条件,我们可以从以下方面获得可见性:使用线程安全的数据结构,通过使用不可变对象(immutable对象)获得原子性,最后通过搭载 ConcurrentHashMap的线程安全性进行排序。
    总结一下整个答案,我会说多线程是一个很难掌握的专业,我自己绝对不是。通过了解使程序具有线程安全性的概念,并考虑JMM允许和保证的内容,可以确保您的代码将执行您希望执行的操作。多线程代码中的错误通常是由于JMM允许在其参数范围内产生违反直觉的结果而不是JIT进行性能优化而导致的。如果您阅读了所有内容,希望您会学到更多有关多线程的知识。线程安全应该通过建立线程安全范式的集合来实现,而不是使用规范上的不便之处(Lea或Bloch,甚至不确定谁说了这一点)。

    关于java - 解释JIT重新排序的工作方式,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42079959/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com