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

构造函数中的同步以使其发生在之前

高勇
2023-03-14

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

我读过很多书,说在构造函数中编写同步作用域没有意义,但为什么没有呢?是的,只要构造中的对象不在线程之间共享(不应该共享),除了构造线程之外,没有其他线程可以访问任何同步的(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创建一个计数器对象,另一个线程t1使用它。如果构造函数中有synchronized语句,那么显然可以保证它是线程安全的。(因为同步作用域中的所有操作彼此之间都有“先发生后发生”的关系。)但如果不是,即没有同步语句,Java内存模型是否仍能保证计数t0的初始化写入可以被t1看到?我想不是。这就像f.y可以在JLS中的示例代码17.5-1中看到0一样。17.5. 与JSL的情况不同。17.5-1,现在第二个线程只从同步方法访问字段,但我认为同步语句在这种情况下没有保证效果。(它们不会与t0的任何动作建立任何先发生后发生的关系。)有人说,关于a的规则发生在构造函数末尾的边之前,这保证了它的存在,但该规则似乎只是说构造函数发生在finalize()之前。

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

或者我对线程安全的定义和并发策略的理解是错误的吗?如果我通过线程安全的方式将计数器对象从t0传输到t1,例如通过易失性变量,似乎没有问题。(在这种情况下,t0的构造发生在易失性写入之前,发生在t1读取的易失性之前,发生在t1对它所做的一切之前。)我是否应该始终通过导致发生前关系的方式在线程之间传输线程安全对象(但不是不可变的)?

共有1个答案

巢权
2023-03-14

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

几年前关于java并发兴趣列表有一个有点长的讨论;我将在这里提供摘要。(完全披露:我开始了那个讨论,并参与了整个讨论。)

请记住,“先发制人”边仅适用于释放锁的一个线程和获取锁的后续线程之间。那么,假设您有:

someNonVolatileField = new Foo();

这里有三套重要的行动:

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

假设另一个线程使用该引用,并调用一个同步的doFoo()方法。现在,我们又添加了两个操作:

由于发布到某个非易失性字段是不安全的,因此系统可以进行许多重新排序。特别是,允许阅读线程按以下顺序查看发生的事情:

  1. 正在分配的对象,其所有字段都设置为0/null

在这种情况下,仍然有一个发生在边缘之前,但与您想要的相反。具体地说,对doFoo()的调用正式发生在构造函数之前。

这确实给你带来了一点好处;这意味着任何同步方法(或块)都可以保证看到构造函数的全部效果,或者没有这些效果;它不会只看到构造函数的一部分。但在实践中,您可能希望确保看到构造函数的效果;毕竟,这就是您编写构造函数的原因。

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

 类似资料:
  • 问题内容: 某个地方的人告诉我Java构造函数是同步的,因此在构造过程中不能同时访问它,而我在想:是否有构造函数将对象存储在映射中,而另一个线程在构造之前从该映射检索它完成后,该线程是否会阻塞,直到构造函数完成? 让我用一些代码演示: 假设put / get是地图上唯一的操作,因此我不会通过迭代之类的方法来获取CME,并尝试在此忽略其他明显的缺陷。 我想知道的是,如果另一个线程(显然不是构造该对象

  • 问题内容: 如果我有一个像这样的构造函数: 然后,我如何在与构造函数相同的类中的方法中使用变量c和d,因为尝试仅在方法中使用变量名似乎不起作用? 问题答案: 实际上,您的代码将无法编译- 无效。 我认为您的意思是:- 。 然后我如何在与构造函数相同的类中的方法中使用变量c和d 您不能这样做,因为您已将它们声明为局部变量,其范围在构造函数结束执行时终止。 您应该将它们声明为实例变量。

  • 我们不能使构造函数,但可以在构造函数内部编写。在什么情况下会有这样的要求?我很开心。

  • 问题内容: 我如何最好地处理以下情况? 我有一个构造函数,需要一些时间才能完成。 我看到了三个选项,每个选项似乎都与众不同。 一种 ,向构造函数添加回调。 第二 ,使用EventEmitter发出“已加载”事件。 或三 ,阻止构造函数。 但我以前从未见过任何完成的事情。 我还有什么其他选择? 问题答案: 鉴于有必要避免在Node中进行阻塞,事件或回调的使用并不奇怪(1)。 稍加修改为2,即可将其与

  • 由于派生类继承了其基类的成员,所以在建立派生类的实例对象时,必须调用基类的构造函数来初始化派生类对象的基类成员。派生类的构造函数既可以隐式调用基类的构造函数,也可以在派生类的构造函数中通过给基类提供初始化值(利用了前面所讲过的成员初始化值语法)显式地调用基类的构造函数。 派生类不继承基类的构造函数和赋值运算符,但是派生类的构造函数和赋值运算符能调用基类的构造函数和赋值运算符。 派生类的构造函数总是

  • 问题内容: 嘿,我对函数的原型和固有性有疑问。您能否解释一下如何从构造函数返回arr并将此arr添加到原型中? 并且在this.arr中是未定义的。有角度的工厂以及前端和后端之间的连接 问题答案: 将异步操作放入构造函数中特别困难。这有几个原因: 构造函数需要返回新创建的对象,因此它不能返回将告诉您异步操作何时完成的承诺。 如果在构造函数内进行异步操作以设置一些实例数据,并且构造函数返回对象,则调