构造函数中的同步使其发生之前 [英] Synchronization in Constructors to make it Happens-before

查看:103
本文介绍了构造函数中的同步使其发生之前的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个问题,关于如何使Java内存模型保证对象是线程安全的.

我读了很多书,说在构造函数中编写同步作用域没有任何意义,但是为什么不这样做呢?是的,的确,只要正在构造的对象不在线程之间共享(不应共享),除了正在构造的对象之外,没有其他线程可以到达任何同步的(this){...}无需在构造函数中设置该范围即可排除它们.但是,同步作用域不仅要排除在外,还应包括在内.它们还用于创建事前发生的关系. JLS.17.4

这里是一个示例代码,可以使我的观点清楚.

 公共类计数器{私有int计数;公共计数器(int init_value){//已同步(this){this.count = init_value;//}}公共同步int getValue(){返回计数;}公共同步无效addValue(){数++;}} 

考虑线程t0创建Counter对象而另一个线程t1使用它的情况.如果在构造函数中有synced语句,则显然可以保证它是线程安全的.(由于同步作用域中的所有动作之间都具有事前发生的关系.)但是,如果没有,即没有同步语句,Java内存模型是否仍保证t1可以看到count的t0初始化写入?我觉得不是.就像fy可以在

然后我应该在构造函数中编写synced语句以使对象成为线程安全的吗?还是我错过了一些有关Java内存模型的规则或逻辑,实际上并不需要它?如果我是真的,那么即使是openjdk的Hashtable(尽管我知道它已经过时)似乎也不是线程安全的.

对于线程安全的定义和并发策略,我是否错了?如果我通过线程安全的方式将该计数器对象从t0传输到t1,例如通过一个volatile变量,似乎没有问题.(在那种情况下,通过t0进行的构造发生在volatile写入之前,发生在volatile读取之前,在进行t1对它进行任何操作之前,在t1进行volatile读取之前.)我是否应该始终传输甚至线程安全的对象(但不是不变的))之间的关系,从而导致事前发生关系?

如果对象已安全发布(例如,通过将其实例化为 someVolatileField = new Foo()),则您无需不需要在构造函数中进行同步.如果不是,那么构造函数中的同步是不够的.

关于Java并发兴趣列表几年前我将在此处提供摘要.(完全公开:我开始了讨论,并且一直参与其中.)

请记住,before-before边缘仅在释放锁的一个线程与随后的获取锁的线程之间适用.因此,假设您有:

  someNonVolatileField = new Foo(); 

这里有三套重要的动作:

  1. 正在分配的对象,其所有字段均设置为0/null
  2. 正在运行的构造函数,包括获取和释放对象的监视器
  3. 该对象的引用已分配给 someNonVolatileField

比方说,另一个线程然后使用该引用,并调用一个 synchronized doFoo()方法.现在我们再添加两个操作:

  1. 阅读 someNonVolatileField 参考
  2. 调用 doFoo(),其中包括获取和释放对象的监视器

由于发布到某些NonVolatileField并不安全,因此系统可以进行很多重新排序.特别是,允许​​阅读线程按以下顺序查看事情的发生:

  1. 正在分配的对象,其所有字段均设置为0/null
  2. 该对象的引用已分配给 someNonVolatileField
  3. 阅读 someNonVolatileField 参考
  4. 调用 doFoo(),其中包括获取和释放对象的监视器
  5. 正在运行的构造函数,包括获取和释放对象的监视器

在这种情况下,仍然存在一个事前发生的事情,但是与您想要的事情相反.具体来说,对 doFoo()的调用是在构造函数之前正式进行的.

这确实给您带来了一笔一点的收益;这意味着可以保证任何同步方法(或块)都不会看到构造函数的全部效果,也不会看到这些效果;它不会只看到构造函数的一部分.但是在实践中,您可能希望保证您看到了构造函数的效果.毕竟,这就是为什么要编写构造函数的原因.

您可以通过不同步doFoo()来解决此问题,而是设置一些自旋循环,等待表示构造函数已运行的标志,然后手动进行 synchronized(this)块.但是,当您达到那种复杂程度时,最好说此对象是线程安全的假设其初始发布是安全的".对于大多数自称线程安全的可变类,这是事实的假设.不可变的对象可以使用 final 字段,即使面对不安全的发布,该字段也是线程安全的,但不需要显式同步.

I have a question about how to make an object guaranteed to be thread-safe by the Java Memory Model.

I have read a lot which say that writing a synchronized scope in a constructor does not make sense, but why doesn't it? Yes, it is true that as long as the object under construction is not shared among threads (, which it shouldn't be), no threads other than the constructing one can reach any synchronized(this){...}, so there are no need to make that scope in the constructor in order to exclude them. But synchronized scopes are not only for exclusion; they are also used to create happens-before relationships. JLS.17.4

Here is a sample code to make my point clear.

public class Counter{

    private int count;

    public Counter(int init_value){
        //synchronized(this){
            this.count = init_value;
        //}
    }

    public synchronized int getValue(){
        return count;
    }

    public synchronized void addValue(){
        count++;
    }
}

Think about the case where a thread t0 creates a Counter object and another thread t1 uses it. If there were the synchronized statement in the constructor, it would obviously be guaranteed to be thread-safe. (Since all actions in synchronized scopes have a happens-before relationship with each other.) But if not, i.e. no synchronized statement, does the Java Memory Model still guarantee that the initializing write by t0 of count can be seen by t1? I think not. It is just like f.y can see 0 in the sample code 17.5-1 in JLS.17.5. Unlike the case of JSL.17.5-1, now the second thread accesses the field only from synchronized methods, but I think synchronized statements have no guaranteed effect in this situation. (They don't create any happens-before relationship with any action by t0.) Some say that the rule about a happens-before edge at the end of a constructor guarantees it, but the rule seems to be just saying a constructor happens-before finalize().

Then should I write the synchronized statement in the constructor to make the object thread-safe? Or are there some rules or logics about the Java Memory Model I have missed and actually no need for that? If I am true, even Hashtable of openjdk (though i know it is obsolete) seems not to be thread-safe.

Or am I wrong about the definition of being thread-safe and about the policy for concurrency? If I transfer that Counter object from t0 to t1 by a thread-safe way, e.g. through a volatile variable, there seems to be no problem. (In that case, the construction by t0 happens-before the volatile write, which happens-before the volatile read by t1, which happens-before everything t1 does to it.) Should I always transfer even thread-safe objects (but not immutable) among threads through a way that causes a happens-before relationship?

解决方案

If the object is safely published (for instance, by instantiating it as someVolatileField = new Foo()), then you don't need synchronization in the constructor. If it's not, then synchronization in the constructor is not enough.

There was a somewhat lengthy discussion on the java concurrency-interest list a few years back about this; I'll provide the summary here. (Full disclosure: I started that discussion, and was involved throughout it.)

Remember that the happens-before edge only applies between one thread releasing the lock, and a subsequent thread acquiring it. So, let's say you have:

someNonVolatileField = new Foo();

There are are three significant sets of actions here:

  1. the object being allocated, with all its fields set to 0/null
  2. the constructor running, which includes an acquire and release of the object's monitor
  3. the object's reference being assigned to someNonVolatileField

Let's say another thread then uses the reference, and calls a synchronized doFoo() method. Now we add two more actions:

  1. reading the someNonVolatileField reference
  2. invoking doFoo(), which includes an acquire and release of the object's monitor

Since the publication to someNonVolatileField wasn't safe, there is a lot of reordering that the system can do. In particular, the reading thread is allowed to see things happening in this order:

  1. the object being allocated, with all its fields set to 0/null
  2. the object's reference being assigned to someNonVolatileField
  3. reading the someNonVolatileField reference
  4. invoking doFoo(), which includes an acquire and release of the object's monitor
  5. the constructor running, which includes an acquire and release of the object's monitor

In this case, there's still a happens-before edge, but goes the other way around from what you want. Specifically, the call to doFoo() formally happens-before the constructor.

This does buy you a little bit; it means that any synchronized method (or block) is guaranteed to see either the full effects of the constructor, or none of those effects; it won't see only part of the constructor. But in practice, you probably want to guarantee that you see the effects of the constructor; that's why you wrote the constructor, after all.

You can get around this by having doFoo() not be synchronized, and instead set up some spin-loop waiting for a flag that says the constructor has run, followed by a manual synchronized(this) block. But by the time you get to that level of complexity, it's probably better to just say "this object is thread safe assuming its initial publication was safe." That's the de-facto assumption for most mutable classes that bill themselves as thread-safe; immutable ones can use final fields, which is thread-safe even in the face of unsafe publication, but which doesn't require explicit synchronization.

这篇关于构造函数中的同步使其发生之前的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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