【问题标题】:Constructor synchronization in JavaJava中的构造函数同步
【发布时间】:2012-09-18 15:32:54
【问题描述】:

有人告诉我Java构造函数是同步的,因此在构造过程中不能同时访问它,我想知道:如果我有一个将对象存储在映射中的构造函数,并且另一个线程从该映射中检索它在它的构造完成之前,该线程会阻塞直到构造函数完成吗?

让我用一些代码来演示一下:

public class Test {
    private static final Map<Integer, Test> testsById =
            Collections.synchronizedMap(new HashMap<>());
    private static final AtomicInteger atomicIdGenerator = new AtomicInteger();
    private final int id;

    public Test() {
        this.id = atomicIdGenerator.getAndIncrement();
        testsById.put(this.id, this);
        // Some lengthy operation to fully initialize this object
    }

    public static Test getTestById(int id) {
        return testsById.get(id);
    }
}

假设 put/get 是地图上唯一的操作,所以我不会通过迭代之类的方式获得 CME,并尝试忽略此处的其他明显缺陷。

我想知道的是,如果另一个线程(显然不是构造对象的线程)尝试使用getTestById 访问对象并在其上调用某些东西,它会阻塞吗?换句话说:

Test test = getTestById(someId);
test.doSomething(); // Does this line block until the constructor is done?

我只是想澄清一下构造函数同步在 Java 中的进展情况,以及这样的代码是否会出现问题。我最近看到了这样的代码,而不是使用静态工厂方法,我想知道这在多线程系统中有多危险(或安全)。

【问题讨论】:

  • 你在考虑静态初始化吗?一般来说,可变静态和特别是自注册构造函数几乎总是一个坏主意。
  • @TomHawtin-tackline 我知道正确同步的静态工厂方法是首选方法,我只是想知道这段代码是否存在任何功能问题,或者代码是否真的会中断。
  • 我已取消删除我匆忙(以前是错误的)答案@Brian FYI。重要的一点是,当您在 any 对象(或者如果您引用 volatile 字段)上调用 synchronized 时,all 内存在线程之间同步。不仅仅是你synchronized 上的对象。

标签: java multithreading concurrency constructor


【解决方案1】:

有人告诉我,Java 构造函数是同步的,因此在构造过程中不能同时访问它

当然不是这样。与构造函数没有隐含的同步。不仅可以同时发生多个构造函数,而且还可以通过例如在构造函数内部分叉一个线程来引用正在构造的 this 来解决并发问题。

如果我有一个将对象存储在映射中的构造函数,并且另一个线程在其构造完成之前从该映射中检索它,那么该线程会阻塞直到构造函数完成吗?

不会的。

线程应用程序中构造函数的最大问题是编译器有权在 Java 内存模型下对构造函数内部的操作重新排序,以便它们发生在之后(所有事情中)创建对象引用并完成构造函数。 final 字段将保证在构造函数完成时完全初始化,但不会保证其他“正常”字段。

在您的情况下,由于您将 Test 放入同步映射中,并且 然后 继续进行初始化,正如 @Tim 所提到的,这将允许其他线程获取对象可能处于半初始化状态。一种解决方案是使用static 方法来创建您的对象:

private Test() {
    this.id = atomicIdGenerator.getAndIncrement();
    // Some lengthy operation to fully initialize this object
}

public static Test createTest() {
    Test test = new Test();
    // this put to a synchronized map forces a happens-before of Test constructor
    testsById.put(test.id, test);
    return test;
}

我的示例代码有效,因为您正在处理一个同步映射,它调用 synchronized 以确保 Test 构造函数已完成并已实现内存同步。

您的示例中的大问题是“发生之前”保证(构造函数可能在将Test 放入映射之前无法完成)和内存同步(构造线程和获取线程可能会看到不同的内存对于Test 实例)。如果将put 移到构造函数之外,则两者都由同步映射处理。 synchronized 在什么对象上都没有关系,以保证构造函数在放入映射之前已经完成并且内存已经同步。

我相信,如果您在构造函数的 very 末尾调用 testsById.put(this.id, this);,实际上您可能会没事,但这不是好的形式,至少需要仔细的注释/文档。如果类是子类并且在super() 之后的子类中进行了初始化,这不会解决问题。我展示的static 解决方案是一个更好的模式。

【讨论】:

  • 我知道可以同时调用多个构造函数,我不是说类级别的同步,而是对象级别的,比如synchronized(this)。不,同步映射大概只锁定其方法,因此 getput 是原子的,我不会从并发获取/放置中得到奇怪的行为。
  • @Brian 最大的问题是“发生在”保证和内存同步。两者都由同步映射处理。确保构造函数在放入地图之前完成并不重要。
  • 地图已同步这一事实如何阻止某人获得对部分初始化的类实例的引用?
  • 因为SynchronizedMap 调用synchronized。当您越过该内存障碍时,会发生在保证之前发生的事情。因此编译器无法重新排序指令过去 synchronized 块。所以构造函数必须在 put() 之前完成。
  • 正如@DaoWen 所说,地图同步于自身,而不是我的对象,如果我的对象被放入地图之前我的构造函数完成(看看,一个冗长的操作发生在 put 之后),对get 的调用可以返回它的引用并且它不会阻塞(因为同步监视器不会在引用传递时检查,只有解引用/方法调用)。但我明白你的意思不是这样。
【解决方案2】:

你被误导了。您所描述的实际上被称为不当发布,并在 Java Concurrency In Practice 一书中进行了详细讨论。

所以是的,另一个线程有可能获得对您的对象的引用并在它完成初始化之前开始尝试使用它。但是等等,考虑这个答案会变得更糟:https://stackoverflow.com/a/2624784/122207 ...基本上可以对引用分配和构造函数完成进行重新排序。在引用的示例中,一个线程可以在新实例上分配h = new Holder(i),另一个线程调用h.assertSanity(),时机恰到好处,以便为Holder 的构造函数中分配的n 成员获取两个不同的值。

【讨论】:

  • Gray 删除了他的答案,因此您可能需要更改对它的引用。
  • 感谢您的回答,蒂姆。我几乎不想接受和毁掉你的 6666 代表 :) 另外,谢谢你提醒我去找那本书。我们在上一份工作中拥有它,但我从未读过它,我几乎忘记了它。
  • NP 布赖恩。 @DaoWen,真可惜,我觉得他的回答更好。
  • @Brian,我突然想到您可能一直在回忆与静态成员分配和静态块执行相关的行为 (static { /*code here*/ })。这些操作由ClassLoader 执行,并且保证只发生一次(每个类加载器)并且在发布该类的任何实例之前发生。一种可以被描述为“类似同步”的行为。
  • 我已经重新添加了我的正​​确答案。我正冲出门,错过了帖子的关键部分。很抱歉造成混乱。
【解决方案3】:

这是不安全的。 JVM 中没有额外的同步。你可以这样做:

public class Test {
    private final Object lock = new Object();
    public Test() {
        synchronized (lock) {
            // your improper object reference publication
            // long initialization
        }
    }

    public void doSomething() {
        synchronized (lock) {
            // do something
        }
    }
}

【讨论】:

  • 这是错误的。如果另一个线程在doSomething()构造函数获得锁之前获得了锁怎么办?不正确的参考发布需要在锁定下发生。但是,即使在修复它之后,我也不推荐这种方法。如果您稍后扩展 Test 类并且应该在锁定下发生更多初始化怎么办?有太多事情可能出错。
  • 我同意。但在某些特定情况下(例如私有类)可以应用这种解决方案。
  • 好的,既然你修复了同步部分,我已经删除了我的反对票。如果您假设永远不会调用 super(),那么您可能应该将类声明为 final 以确保。
【解决方案4】:

构造函数和其他方法一样,没有额外的同步(除了处理final字段)。

如果this 稍后发布,代码将起作用

public Test() 
{
    // Some lengthy operation to fully initialize this object

    this.id = atomicIdGenerator.getAndIncrement();
    testsById.put(this.id, this);
}

【讨论】:

  • 您可能想要解释原子访问充当内存屏障,因此它发生在put 之前很重要。此外,如果任何类继承自 Test 并调用 super(),这仍然是不安全的。
  • @DaoWen 我什至没有想到继承。我想最好的选择肯定是某种静态初始化,在结果确定之后缓存结果(例如直接在调用new之后),对吗?
  • @Brian - 我相信你是对的。在构造时发布类实例的唯一真正安全的方法是使用工厂方法构造它,这将允许您仅在它实际完成后发布它。另外,我认为我所说的AtomicInteger 访问很重要实际上是错误的,因为他正在执行put 的地图已经同步。
【解决方案5】:

有人告诉我 Java 构造函数是同步的

“某个地方的某个人”被严重误导。构造函数不同步。证明:

public class A
{
    public A() throws InterruptedException
    {
        wait();
    }

    public static void main(String[] args) throws Exception
    {
        A a = new A();
    }
}

此代码在wait() 调用中抛出java.lang.IllegalMonitorStateException。如果同步有效,则不会。

这甚至没有意义。它们不需要同步。只能在new(), 之后调用构造函数,并且根据定义,new() 的每次调用都返回不同的值。因此,构造函数被两个线程同时调用且具有相同this 值的可能性为零。所以不需要构造函数的同步。

如果我有一个将对象存储在映射中的构造函数,并且另一个线程在其构造完成之前从该映射中检索它,那么该线程会阻塞直到构造函数完成吗?

没有。为什么会这样做?谁来阻止它?让 'this' 从这样的构造函数中逃逸是不好的做法:它允许其他线程访问仍在构造中的对象。

【讨论】:

  • 澄清一下,我的问题是关于对象级(即synchronized(this))而不是类级(synchronized(Test.class))的构造函数同步。我本质上是在问,“如果另一个线程在它仍在构造的时候得到了我的Test 引用,它可以在不被阻塞的情况下调用方法吗?”显然,答案是肯定的,不会阻塞其他线程,所以+1
  • @Brian 我明白你的问题。由于构造函数不可能同时或甚至顺序地执行两次相同的 'this' 值,因此不可能出现对 'this' 进行同步的需要。
  • 当然可以。想象一下:Test 有一个内部成员类TestThread,它调用创建它的Test 上的方法。在我的Test 构造函数中,我在执行一些冗长的操作之前创建并启动了TestThread。如果Test 的构造函数没有同步,TestThreadTest 上运行方法,而Test 仍在构造中。如果我在其构造函数中同步Test,线程必须等到构造函数完成才能调用其方法。这有意义吗?
  • @Brian 当然有道理,但这不是两个线程同时在同一个对象上调用同一个构造函数的例子。
  • 你是对的,它不是。相反,它是一个线程调用构造函数而另一个线程访问它的示例。 这就是我的问题:是否有任何同步机制来处理不正确的参考发布?我知道你不能两次调用一个对象的构造函数,这很明显。请重新阅读我的问题并查看所选答案。
【解决方案6】:

虽然这个问题得到了回答,但是粘贴的代码没有遵循安全的构造技术,因为它允许这个引用从构造函数中逃逸,我想分享一个由 Brian Goetz 提供的漂亮解释在article: "Java theory and practice: Safe construction techniques" at the IBM developerWorks website

【讨论】:

  • 通常不鼓励提供没有摘要的链接的答案。来自help page:“鼓励链接到外部资源,但请在链接周围添加上下文,以便您的其他用户了解它是什么以及为什么存在。始终引用重要链接中最相关的部分,以防万一目标站点无法访问或永久脱机。”
猜你喜欢
  • 1970-01-01
  • 2011-06-20
  • 2017-04-14
  • 2021-12-30
  • 2013-12-27
  • 2012-08-05
  • 2018-07-11
  • 2016-06-15
相关资源
最近更新 更多