5

我正在使用 joda,因为它在多线程方面享有盛誉。使多线程日期处理高效,例如通过使所有 Date/Time/DateTime 对象不可变,还有很长的路要走。

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

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

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

所有格式化程序都是线程安全的(它们也是不可变的),但是格式化程序工厂是什么:

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 中,另一个丢失,迟早会被垃圾收集。

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

(在说我疯之前,请阅读这篇维基百科文章中的类似情况。)

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

感谢您的解释!

雷托

4

4 回答 4

4

你说,格式化程序是只读的。如果他们只使用最终字段(我没有阅读格式化程序源),那么在 Java 语言规范的第 3 版中,他们会受到“最终字段语义”的保护,以免创建部分对象。我没有检查 2-nd JSL 版本,也不确定这种初始化在那个版本中是否正确。

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

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

有时,在另一个线程中,调用 dateTime().format 发生。那时我们需要两次阅读。两者中的第一个是读取格式化程序对象中的最终变量。我们称它为 r2(与 JLS 一致)。两者中的第二个是读取格式化程序的“this”。这发生在读取 dt 字段时调用 dateTime() 方法期间。让我们称之为读取 r1。我们现在有什么?读取 r1 看到一些写入 dt。我认为该写入是上一段中的操作 a (仅为了简单起见,只有一个线程编写了该字段)。如 r1 所见写 a,则有 mc(a, r1)(“记忆链”关系,第一个子句定义)。当前线程没有初始化格式化程序,在操作 r2 中读取它的字段并看到在操作 r1 读取的格式化程序的“地址”。因此,

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

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

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

于 2010-03-24T19:28:06.277 回答
0

这有点无法回答,但最简单的解释是

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

可能只是他们没有确保任何事情,或者开发人员没有意识到这可能是一个错误,或者认为它不值得同步。

于 2010-03-24T17:37:11.657 回答
0

我在 2007 年的 Joda 邮件列表上问了一个类似的问题,尽管我没有找到确切的答案,因此我避开了 Joda 时间,无论好坏。

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

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

于 2010-12-13T15:22:39.417 回答
-1

IMO 最坏的情况不是创建两个对象,而是创建多个对象(dateTime()准确地说,与调用线程一样多)。由于dateTime()不是同步的,dt既不是最终的也不是易失的,因此不能保证在一个线程中对其值的更改对其他线程是可见的。因此,即使在一个线程初始化之后dt,任何数量的其他线程仍可能将引用视为 null,从而愉快地创建新对象。

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

于 2010-03-24T17:27:44.550 回答