2010-03-24 71 views
5

我使用joda,因为它在多线程方面的声誉很好。通过使所有Date/Time/DateTime对象不可变来实现多线程日期处理的效率很高。部分构造的对象/多线程

但是,我不确定乔达是否真的在做正确的事情。它可能会,但我很感兴趣看到一个解释。

当一个DateTime的一个toString()被调用乔达执行以下操作:

/* 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中,另一个会丢失,并且迟早会被垃圾回收。

  • 的objec已完成之前初始化

静态变量可能指向一个部分构造的对象(叫我疯狂之前,阅读有关本Wikipedia article类似的情况。)

所以Joda如何确保部分创建的格式化程序不会在此静态变量中发布?

感谢您的解释!

雷托

回答

4

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

请参阅JLS第17.5章和第17.5.1章。我将为所需要的事情建立一个“事件链”。

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

有时,在另一个线程中,会调用dateTime()。格式。那时我们需要两次读取。首先读取格式化程序对象中的最终变量。我们称之为r2(与JLS保持一致)。其中的第二个是Formatter的“this”的读取。这发生在读取dt字段时调用dateTime()方法期间。我们称之为r1。我们现在有什么?阅读r1看到一些写给dt。我认为这个写作是上一段的行动(只有一个线程写了这个字段,只是为了简单)。由于r1参见写a,则存在mc(a,r1)(“内存链”关系,第一个子句定义)。当前线程没有初始化格式化程序,在操作r2中读取它的字段,并在操作r1处看到格式化程序的“地址”。因此,根据定义,有一个解引用(r1,r2)(从JLS排序的另一个动作)。

我们在冻结前写过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(唯一从程序代码写入该字段)。

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

但这不是全部!没有访问偏好构造的对象。但有一个更有趣的错误。缺少任何显式的synhronization dateTime()可能返回null。我不认为这种行为在实践中可以观察到,但是JLS第3版不能阻止这种行为。方法中的第一次读取dt字段可能会看到一个由另一个线程初始化的值,但dt的第二次读取可以看到“写入defalut值”。没有发生 - 在关系存在以防止它。这种可能的行为是特定于第三版的,第二版具有“写入主存储器”/“从主存储器读取”,这不允许线程看到时间倒数的值。

+0

+1。这是java并发模型中为什么不可变对象更“简单”的另一个原因。但是,我不确定最后一段,请参阅JLS 17.4.4:“向每个变量写入默认值(零,false或null) - 与每个线程中的第一个操作同步”。在读取dt之前发生“写入默认值”是必要的。 – irreputable 2010-03-24 20:45:43

+0

是的,在读取dt之前写入默认值。这可以防止线程读取“垃圾”值。但是在另一个线程中写入字段(初始化dt字段)不与dt读取具有hb关系。因此,我们可以随时看到这两个写入中的任何一个(默认或来自初始化dt的线程)。我们必须读取默认初始值的唯一时间是临时性需求检查。但是那时我们只提交一个默认值的读取,并且在下一次迭代中说,该线程将一个对象写入dt(仅用于该方法中的第一次读取)。 – maxkar 2010-03-25 06:14:33

-1

IMO最坏的情况是越来越创建不是两个对象,但几个(多达有线程调用dateTime(),要准确)。由于dateTime()未同步,且dt既不是最终的也不是易失性的,因此不保证其在一个线程中的值更改对其他线程可见。因此,即使在一个线程初始化为dt之后,其他任何线程仍然可以将引用视为null,因此可以愉快地创建新对象。

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

0

这是一个有点不回答,但对于

最简单的解释那么,如何乔达确保不部分创建的格式被发表在静态变量?

可能只是因为他们没有确定任何东西,开发人员没有意识到它可能是一个bug或者认为它不值得同步。

0

I asked a similar question在2007年Joda邮件列表中,虽然我没有找到答案是确凿的,我避免了乔达时间,因此无论好坏。

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

IIRC,版本2 of JLS没有包含关于对象引用的明确说明,也就是说只有32位ref是原子保证的,所以如果你使用的是64位JVM,则不能保证它可以工作。当时我正在使用JLS v3之前的Java 1.4。