在实践中,对象构造是否可以保证所有线程都看到初始化的非最终字段? [英] Does object construction guarantee in practice that all threads see non-final fields initialized?

查看:72
本文介绍了在实践中,对象构造是否可以保证所有线程都看到初始化的非最终字段?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

The Java memory model guarantees a happens-before relationship between an object's construction and finalizer:

从 对象指向该对象的终结器(§12.6)的开头.

There is a happens-before edge from the end of a constructor of an object to the start of a finalizer (§12.6) for that object.

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

As well as the constructor and the initialization of final fields:

当一个对象被认为是完全初始化的 构造函数完成.只能看到引用的线程 该对象已完全初始化之后的对象得到保证 查看该对象的final的正确初始化的值 字段.

An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.

对于volatile字段也有保证,因为关于对这些字段的所有访问都存在事前发生的关系:

There's also a guarantee about volatile fields since, there's a happens-before relations with regard to all access to such fields:

在随后的每个后续操作之前,都会对volatile字段(第8.3.1.4节)进行写操作 阅读该字段.

A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.

但是,常规的,良好的旧非易失性字段呢?我已经看到了很多多线程代码,这些对象在使用非易失性字段构造对象之后不会麻烦创建任何类型的内存屏障.但是由于这个原因,我从未见过或听说过任何问题,而且我自己也无法重新创建这种局部构造.

But what about regular, good old non-volatile fields? I've seen a lot of multi-threaded code that doesn't bother creating any sort of memory barrier after object construction with non-volatile fields. But I've never seen or heard of any issues because of it and I wasn't able to recreate such partial construction myself.

现代JVM是否在构造后就设置了内存屏障?避免在施工前后重新排序?还是我只是幸运?如果是后者,是否可以编写可随意复制部分构造的代码?

Do modern JVMs just put memory barriers after construction? Avoid reordering around construction? Or was I just lucky? If it's the latter, is it possible to write code that reproduces partial construction at will?

为澄清起见,我正在谈论以下情况.假设我们有一堂课:

To clarify, I'm talking about the following situation. Say we have a class:

public class Foo{
    public int bar = 0;

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

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

And some Thread T1 instantiates a new Foo instance:

Foo myFoo = new Foo();

然后将实例传递给其他线程,我们将其称为T2:

Then passes the instance to some other thread, which we'll call T2:

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

T1执行了两次有趣的写操作:

T1 performed two writes that are interesting to us:

  1. T1将值5写入新实例化的myFoo
  2. bar
  3. T1将对新创建对象的引用写到myFoo变量
  1. T1 wrote the value 5 to bar of the newly instantiated myFoo
  2. T1 wrote the reference to the newly created object to the myFoo variable

对于T1,我们得到一个保证写入#1 发生之前写入#2:

For T1, we get a guarantee that write #1 happened-before write #2:

线程中的每个动作发生在该线程之前 会按照程序的顺序出现.

Each action in a thread happens-before every action in that thread that comes later in the program's order.

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

But as far as T2 is concerned the Java memory model offers no such guarantee. Nothing prevents it from seeing the writes in the opposite order. So it could see a fully built Foo object, but with the bar field equal to equal to 0.

Edit2

写完几个月后,我再次看了一下上面的例子.自从T1写入后启动T2以来,实际上可以保证该代码正常工作.这使它成为我要提出的问题的错误示例.修复了假设T1在执行写操作时T2已经在运行的问题.假设T2正在循环读取myFoo,就像这样:

I took a second look at the example above a few months after writing it. And that code is actually guaranteed to work correctly since T2 was started after T1's writes. That makes it an incorrect example for the question I wanted to ask. The fix it to assume that T2 is already running when T1 is performing the write. Say T2 is reading myFoo in a loop, like so:

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

推荐答案

以您的示例作为问题本身-答案是,这是完全可能的.就像您引用的那样,初始化的字段仅对构造线程可见( ).这称为安全出版物(但我敢打赌,您已经对此有所了解).

Taking your example as the question itself - the answer would be yes, that is entirely possible. The initialized fields are visible only to the constructing thread, like you quoted. This is called safe publication (but I bet you already knew about this).

您没有通过实验看到的事实是x86上的AFAIK(是一种强大的内存模型),存储仍然重新排序,因此,除非JIT会对这些存储重新排序T1做到的-您看不到.但这真是火上浇油,这问题及其后续操作(几乎相同)

The fact that you are not seeing that via experimentation is that AFAIK on x86 (being a strong memory model), stores are not re-ordered anyway, so unless JIT would re-ordered those stores that T1 did - you can't see that. But that is playing with fire, literately, this question and the follow-up (it's close to the same) here of a guy that (not sure if true) lost 12 milion of equipment

JLS仅保证实现可见性的几种方法.而且,绕过btw并非如此,JLS不会说何时会中断,而会说何时会起作用.

The JLS guarantees only a few ways to achieve the visibility. And it's not the other way around btw, the JLS will not say when this would break, it will say when it will work.

1)最终字段语义

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

Notice how the example shows that each field has to be final - even if under the current implementation a single one would suffice, and there are two memory barriers inserted (when final(s) are used) after the constructor: LoadStore and StoreStore.

2)易失字段(并隐式为AtomicXXX);我认为这个不需要任何解释,似乎您引用了这个.

2) volatile fields (and implicitly AtomicXXX); I think this one does not need any explanations and it seems you quoted this.

3)静态初始化程序好吧,应该是显而易见的IMO

3) Static initializers well, kind of should be obvious IMO

4)一些锁定-规则发生之前,这也应该很明显...

4) Some locking involved - this should be obvious too, happens-before rule...

这篇关于在实践中,对象构造是否可以保证所有线程都看到初始化的非最终字段?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆