部分构造的对象/多线程

时间:2010-03-24 17:15:32

标签: java multithreading jodatime

由于它在多线程方面的良好声誉,我使用的是joda。通过使所有Date / Time / DateTime对象不可变,使得多线程日期处理变得高效,距离很远。

但是在这种情况下,我不确定Joda是否真的在做正确的事情。它可能会,但我很想看到解释。​​

当调用DateTime的toString()时,Joda会执行以下操作:

/* org.joda.time.base.AbstractInstant */
public String toString() {
    return ISODateTimeFormat.dateTime().print(this);
}

所有格式化程序都是线程安全的(它们也是不可变的),但是有关formatter-factory的内容:

private static DateTimeFormatter dt;

/*  org.joda.time.format.ISODateTimeFormat */
public static DateTimeFormatter dateTime() {
    if (dt == null) {
        dt = new DateTimeFormatterBuilder()
            .append(date())
            .append(tTime())
            .toFormatter();
    }
    return dt;
}

这是单线程应用程序中的常见模式,但众所周知,它在多线程环境中容易出错。

我看到以下危险:

  • 空检查期间的竞争条件 - >最糟糕的情况:创建了两个对象。

没问题,因为这只是一个辅助对象(与正常的单例模式情况不同),一个在dt中保存,另一个丢失,迟早会被垃圾收集。

  • 静态变量可能在objec完成初始化之前指向部分构造的对象

(在叫我疯了之前,先阅读Wikipedia article中的类似情况。)

那么Joda如何确保在这个静态变量中没有发布部分创建的格式化程序?

感谢您的解释!

雷托

4 个答案:

答案 0 :(得分:4)

你说,格式化程序是只读的。如果他们只使用最终字段(我没有阅读格式化程序源代码),那么在第3版Java语言规范中,他们可以通过“最终字段语义”来保护部分对象。我没有检查第二版JSL版本,也不确定,如果该版本中的这种初始化是正确的。

查看JLS中的第17.5和17.5.1节。我将为所需的事先关系构建一个“事件链”。

首先,在构造函数的某处,有一个写入格式化程序中的最后一个字段。这是写w。当构造函数完成时,一个“冻结”动作就开始了。我们称之为f。稍后在程序顺序的某个地方(从构造函数返回之后,可能是其他一些方法并从toFormatter返回)有一个写入dt字段。让我们给这个写一个名字a。这个write(a)是在“程序顺序”中的冻结动作(f)之后(单线程执行中的顺序),因此f仅在JLS定义之前发生在(hb(f,a))之前。 Phew,初始化完成......:)

稍后,在另一个线程中,会发生对dateTime()。格式的调用。那时我们需要两次读取。首先读取格式化程序对象中的最终变量。我们称之为r2(与JLS一致)。这两个中的第二个是对格式化程序的“this”的读取。在读取dt字段时,在调用dateTime()方法期间会发生这种情况。让我们称之为读取r1。我们现在有什么?读r1看到一些写入dt。我认为该写操作是前一段中的操作(只有一个线程编写该字段,只是为了简单起见)。当r1看到写a时,则有mc(a,r1)(“Memory chain”关系,第一个子句定义)。当前线程没有初始化格式化程序,在动作r2中读取它并看到在动作r1处读取的格式化程序的“地址”。因此,根据定义,有一个解除引用(r1,r2)(从JLS排序的另一个动作)。

我们在冻结之前写了,hb(w,f)。在分配dt,hb(f,a)之前我们已经冻结了。我们读取了dt,mc(a,r1)。我们在r1和r2之间有一个解引用链,解引用(r1,r2)。所有这些都只是通过JLS定义导致之前发生的关系hb(w,r2)。此外,根据定义,hb(d,w)其中d是对象中最终字段的默认值的写入。因此,读取r2看不到写入w并且必须看到写入r2(从程序代码中唯一写入字段)。

相同的是更多间接字段访问的顺序(存储在最终字段中的对象的最终字段等)。

但这不是全部!无法访问部分构造的对象。但是有一个更有趣的错误。在缺少任何显式同步时,dateTime()可能返回null。我不认为在实践中可以观察到这种行为,但JLS第3版并不能阻止这种行为。首先读取方法中的dt字段可能会看到另一个线程初始化的值,但是dt的第二次读取可以看到“写入defalut值”。没有发生 - 在存在关系之前阻止它。这种可能的行为特定于第3版,第二版具有“写入主存储器”/“从主存储器读取”,这使得线程无法及时查看变量值。

答案 1 :(得分:0)

这是一个非答案,但对

最简单的解释
  

那么Joda如何确保未在此静态变量中发布部分创建的格式化程序?

可能只是因为他们没有确保任何东西,要么开发人员没有意识到它可能是一个错误或者觉得它不值得同步。

答案 2 :(得分:0)

我在2007年的Joda邮件列表上asked a similar question,虽然我没有找到答案是最终结果,但我无论如何都避免了Joda时间,无论好坏。

Java语言规范的第3版保证对象引用更新是原子的,无论它们是32位还是64位。这与上面列出的论点相结合,使Joda代码成为线程安全的IMO(参见java.sun.com/docs/books/jls/third_edition/html/memory.html#17.7)

IIRC,JLS的第2版没有包含关于对象引用的相同的明确说明,即只有32位引用被保证是原子的,所以如果你使用的是64位JVM,则无法保证它能够正常工作。当时我使用的是Java 1.4,它早于JLS v3。

答案 3 :(得分:-1)

IMO最糟糕的情况不是两个对象被创建而是几个(确切地说,有多个线程正在调用dateTime())。由于dateTime()未同步且dt既不是final也不是volatile,因此不保证在一个线程中对其值的更改对其他线程可见。因此,即使在一个线程初始化dt之后,任何数量的其他线程仍然可以将引用视为null,从而愉快地创建新对象。

除此之外,正如其他人所解释的那样,部分创建的对象无法由dateTime()发布。也不能部分更改(=悬空)引用,因为参考值更新保证是原子的。