构造函数中的同步使其发生 -

时间:2016-09-08 14:24:28

标签: java multithreading concurrency synchronization

我有一个关于如何通过Java内存模型确保对象保证是线程安全的问题。

我已经阅读了很多内容,说在构造函数中编写同步作用域没有意义,但为什么不呢?是的,确实只要构造中的对象不在线程之间共享(它不应该是这样),除了构造对象之外的任何线程都不能到达任何synchronized(this){...},所以不需要在构造函数中创建该范围以排除它们。但同步范围不仅仅是排除;它们也用于创造先发生过的关系。 JLS.17.4

这是一个示例代码,以明确我的观点。

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++;
    }
}

考虑线程t0创建Counter对象而另一个线程t1使用它的情况。如果构造函数中有synchronized语句,那么显然可以保证它是线程安全的。 (因为同步作用域中的所有操作都具有相互之间发生的关系。)但如果不是,即没有同步语句,Java内存模型是否仍然保证t1可以看到t0的初始化写入计数?我想不是。就像f.y可以在JLS.17.5中的示例代码17.5-1中看到0。与JSL.17.5-1的情况不同,现在第二个线程仅从同步方法访问该字段,但我认为在这种情况下,synchronized语句没有保证效果。 (他们不会在t0之前创建任何与任何动作发生的关系。)有人说在构造函数结束时关于发生前的边缘的规则保证了它,但规则似乎只是说构造函数发生了 - 在finalize()之前。

那么我应该在构造函数中编写synchronized语句以使对象是线程安全的吗?或者是否有一些关于我错过的Java内存模型的规则或逻辑,实际上不需要它?如果我是真的,即使是openjdk的Hashtable(虽然我知道它已经过时)似乎不是线程安全的。

或者我对线程安全的定义和并发策略的错误?如果我通过线程安全的方式将Counter对象从t0传输到t1,例如通过一个易变的变量,似乎没有问题。 (在这种情况下,t0的构造发生在易失性写入之前,这发生在t1的易失性读取之前,发生在t1所做的一切之前。)我应该总是传递线程安全对象(但不是永久不变的)通过一种导致事先发生关系的方式在线程中?

1 个答案:

答案 0 :(得分:7)

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

关于这个问题,关于java并发利益列表a few years back的讨论有些冗长;我在这里提供摘要。 (完全披露:我开始讨论,并参与其中。)

请记住,before-before edge仅适用于释放锁的一个线程和获取它的后续线程。所以,让我们说你有:

someNonVolatileField = new Foo();

这里有三套重要的行动:

  1. 正在分配的对象,其所有字段都设置为0 / null
  2. 构造函数正在运行,其中包括对象监视器的获取和释放
  3. 将对象的引用分配给someNonVolatileField
  4. 让我们说另一个线程然后使用引用,并调用synchronized doFoo()方法。现在我们再添加两个动作:

    1. 阅读someNonVolatileField参考
    2. 调用doFoo(),其中包括获取和释放对象的监视器
    3. 由于对某些非易失性字段的发布并不安全,因此系统可以进行大量重新排序。特别是,允许​​读取线程按以下顺序查看事件:

      1. 正在分配的对象,其所有字段都设置为0 / null
      2. 将对象的引用分配给someNonVolatileField
      3. 阅读someNonVolatileField参考
      4. 调用doFoo(),其中包括获取和释放对象的监视器
      5. 构造函数正在运行,其中包括对象监视器的获取和释放
      6. 在这种情况下,仍有一个发生在前的边缘,但反过来你想要的。具体来说,对doFoo()的调用正式发生在构造函数之前。

        这确实会给你一个位;这意味着任何同步的方法(或块)都可以保证看到构造函数的完整效果,或者看不到这些效果;它不会只看到构造函数的一部分。但实际上,您可能希望保证看到构造函数的效果;毕竟,这就是你编写构造函数的原因。

        你可以通过让doFoo()不同步来解决这个问题,而是设置一些旋转循环,等待一个表示构造函数已经运行的标志,然后是一个手动synchronized(this)块。但是当你达到这种复杂程度时,最好只说'#34;这个对象是线程安全的假设它的初始发布是安全的。"这是大多数可变类的事实上的假设,它们将自己称为线程安全的;不可变的字段可以使用final字段,即使面对不安全的发布,它也是线程安全的,但不需要显式同步。