当前位置: 首页 > 知识库问答 >
问题:

对象构造是否在实践中保证所有线程都看到非最终字段已初始化?

蔚弘量
2023-03-14

Java内存模型保证了对象的构造和终结器之间的发生前关系:

从对象的构造函数的末尾到该对象的终结器的开始(§12.6)有一个发生前边缘。

以及构造函数和最终字段的初始化:

当一个对象的构造函数完成时,它被认为是完全初始化的。只有在对象完全初始化后才能看到对该对象的引用的线程才能确保看到该对象最终字段的正确初始化值。

还有一个关于易失性字段的保证,因为对于对这些字段的所有访问,都有一个发生在之前的关系:

对易失性字段(§8.3.1.4)的写入发生在该字段的每次后续读取之前。

但是,对于常规的、好的、旧的非易失性字段呢?我见过很多多线程代码,在使用非易失性字段构造对象之后,它们不会费心创建任何类型的内存障碍。但我从来没有见过或听说过任何问题,因为它,我无法重建这样的部分建设自己。

现代JVM只是在构建之后设置内存障碍吗?避免围绕施工重新排序?还是我只是幸运?如果是后者,是否可以编写任意复制部分构造的代码

编辑:

为了澄清,我说的是以下情况。假设我们有一个类:

public class Foo{
    public int bar = 0;

    public Foo(){
        this.bar = 5;
    }
    ...
}

一些线程T1实例化了一个新的Foo实例:

Foo myFoo=新Foo();

然后将实例传递给另一个线程,我们称之为T2:

Thread t = new Thread(() -> {
     if (myFoo.bar == 5){
         ....
     }
});
t.start();

T1执行了我们感兴趣的两次写入:

  1. T1将值5写入新实例化的myFoo
  2. bar
  3. T1将对新创建对象的引用写入myFoo变量

对于T1,我们保证写#1发生在写#2之前:

线程中的每个动作都发生在该线程中按程序顺序稍后出现的每个动作之前。

但是就T2而言,Java内存模型没有提供这样的保证。没有什么能阻止它看到相反顺序的写入。所以它可以看到一个完全构建的Foo对象,但是bar字段等于0。

编辑2:

在写了几个月后,我又看了一遍上面的例子。由于T2是在T1写入之后启动的,因此该代码实际上可以保证正常工作。这对于我想问的问题来说是一个错误的例子。修正它,假设T2已经在运行,而T1正在执行写操作。假设T2在循环中读取myFoo,如下所示:

Foo myFoo = null;
Thread t2 = new Thread(() -> {
     for (;;) {
         if (myFoo != null && myFoo.bar == 5){
             ...
         }
         ...
     }
});
t2.start();
myFoo = new Foo(); //The creation of Foo happens after t2 is already running

共有3个答案

锺星洲
2023-03-14

在对象构造过程中似乎没有同步。

JLS不允许这样做,我也无法在代码中产生任何迹象。然而,有可能产生反对意见。

运行以下代码:

public class Main {
    public static void main(String[] args) throws Exception {
        new Thread(() -> {
            while(true) {
                new Demo(1, 2);
            }
        }).start(); 
    }
}

class Demo {
    int d1, d2;

    Demo(int d1, int d2) {
        this.d1 = d1;   

        new Thread(() -> System.out.println(Demo.this.d1+" "+Demo.this.d2)).start();

        try {
            Thread.sleep(500);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }

        this.d2 = d2;   
    }
}

输出将连续显示10,证明创建的线程能够访问部分创建的对象的数据。

但是,如果我们同步这个:

Demo(int d1, int d2) {
    synchronized(Demo.class) {
        this.d1 = d1;   

        new Thread(() -> {
            synchronized(Demo.class) {
                System.out.println(Demo.this.d1+" "+Demo.this.d2);
            }
        }).start();

        try {
            Thread.sleep(500);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }

        this.d2 = d2;   
    }
}

输出为12,表明新创建的线程实际上将等待锁,与未同步的示例相反。

相关:为什么构造函数不能同步?

南宫泓
2023-03-14

但轶事证据表明,这在实践中并没有发生

要查看此问题,您必须避免使用任何内存屏障。例如,如果您使用任何类型的线程安全集合或某些System.out.println可以防止问题发生。

我以前见过这个问题,尽管我刚刚为x64上的Java8更新161编写的一个简单测试没有显示这个问题。

陶修洁
2023-03-14

以你的例子作为问题本身——答案是肯定的,这是完全可能的。初始化字段仅对构造线程可见,如您引用的。这被称为安全出版物(但我敢打赌你已经知道这一点)。

事实上,你没有通过实验看到这一点,因为x86上的AFAIK(作为一个强大的内存模型),存储无论如何都不会重新排序,所以除非JIT会像T1那样重新排序那些存储——你看不到这一点。但这是在玩火,从字面上看,这个问题和一个(不确定是否属实)丢失了1200万台设备的人的后续行动(接近相同)

JLS只保证了几种实现可见性的方法。顺便说一句,不是相反,JLS不会说什么时候会中断,它会说什么时候会工作。

最终语义学

请注意该示例如何显示每个字段必须是Final-即使在当前实现下,单个字段就足够了,并且在构造函数之后插入了两个内存障碍(当使用Final(s)时):LoadStoreStoreStore

2) 易失性字段(和隐式原子xxx);我认为这一点不需要任何解释,似乎你引用了这一点。

3) 静态初始化器好吧,在IMO中应该是明显的

4)在规则之前,涉及一些锁定——这也应该是显而易见的...

 类似资料:
  • 假设你有这个代码片段 例如,如果编译器决定让它看起来像 这将是一个问题,因为calculateWaitTime()生成一个新线程,该线程可能会将集合视为null或其中没有1。 那么问题又来了,这种重新排序可能吗?或者所有在构造函数之外初始化的最终字段都是在构造函数之前初始化的,或者至少总是由编译器移动到构造函数的顶部

  • 问题内容: 我想知道下面的代码是否有意义,因为编译器会警告“空白的最终字段对象可能尚未初始化”。有更好的方法吗? 问题答案: 我将字段定为final,并强制构造函数将值向上传递:

  • 问题内容: 我今天早些时候在代码中结束了以下场景(我承认这有点怪异,并且从此以后就进行了重构)。当我运行单元测试时,我发现在超类构造函数运行时尚未设置字段初始化。我意识到我不完全了解构造函数/字段初始化的顺序,因此我希望大家能向我解释这些顺序。 JUnit的缩写backtrace如下,我想我期望$ Foo。 设置foo。 问题答案: 是的,在Java中(例如,与C#不同) , 在超类构造函数 之后

  • 我有一个CloudKit应用程序,它基本上是一个带有一个额外功能的主细节设置。任何详细信息对象都可以标记为ActiveNote。当应用程序在iPad上时,只显示这个ActiveNote(没有用户交互)。该应用程序包括通知和订阅,所有数据都在专用数据库的自定义区域中。该应用程序运行良好,但有一个例外。 只有两种记录类型。所有数据都以cnote类型存储。当一个细节项目被选择显示在iPad上时,我会将该

  • 问题内容: 也许这比技术问题更像是一种样式问题,但是我有一个包含多个成员变量的类,并且我想让它起作用,以便在用户第一次创建该类的实例时初始化一些成员变量(即在该功能),我想其他的成员变量从成员函数参数,将稍后被称为定义。所以我的问题是我应该初始化函数中的所有成员变量(并将稍后定义的变量设置为虚拟值)还是初始化函数中的某些成员以及后续函数中的某些成员变量。我意识到这可能很难理解,因此这里有一些示例。

  • 问题内容: 我应该在这样的声明中初始化类字段吗? 还是像这样在setUp()中? 我倾向于使用第一种形式,因为它更简洁,并且允许我使用最终字段。如果我 不需要 使用setUp()方法进行设置,是否仍应使用它,为什么? 澄清: JUnit将每个测试方法实例化一次测试类。这意味着无论我在哪里声明,每次测试都会创建一次。这也意味着测试之间没有时间依赖性。因此,使用setUp()似乎没有任何优势。但是,J