【问题标题】:Is this a better version of Double Check Locking without volatile and synchronization overhead这是没有 volatile 和同步开销的更好版本的 Double Check Locking
【发布时间】:2013-06-18 12:41:54
【问题描述】:

以下代码 sn-p 来自 Effective Java 2nd Edition Double Checked Locking

// 实例字段延迟初始化的双重检查习惯用法

private volatile FieldType field;

FieldType getField() {
    FieldType result = field;
    if (result == null) {  // First check (no locking)
        synchronized(this) {
            result = field;
            if (result == null)// Second check (with locking)  
                field = result = computeFieldValue();
        }
    }
    return result;
}

据我所知,双重检查锁定的主要问题是第二次检查锁定内部的重新排序,以便其他线程可能会看到已设置的字段/结果的值,而这些值实际上可能仍在执行中。为了避免这种情况,我们将字段的引用设置为 volatile 以保证可见性和重新排序。

但这也可以通过下面的代码来实现

private FieldType field; // non volatile
private volatile boolean fence = false;

FieldType getField() {
    if (field == null) {  // First check (no locking) // no volatile read
        synchronized(this) {   //  inside synch block no problem of visibilty will latest           //value  of field 
            if (field == null) {// Second check (with locking)  
                Object obj =  computeFieldValue();
             fence = true; // any volatile write will take. this will make sure statements are //not reorder with setting field as non null.
            field = (FieldType)obj; // this will be only set after computeFieldValue has been //completed fully
           }
        }
    }
    return field;
}

因此,在完成初始化之后,就没有线程需要进行易失性读取或同步开销。请 看看我的假设是否正确?

【问题讨论】:

  • 现在双重检查锁定不被认为是一件坏事吗? en.wikipedia.org/wiki/Double-checked_locking
  • 有一种明确的方法可以避免这种情况:重构您的代码,以便需要双重检查锁定!在 (100 - epsilon)% 的情况下,这是可行的,而且无论如何您都应该这样做。

标签: java multithreading jakarta-ee volatile


【解决方案1】:

JLS (Section 17.4.5) 声明:

“对 volatile 字段的写入(第 8.3.1.4 节)发生在之后每次读取该字段。”

您没有阅读更新后的fence 变量,因此更新fence 的线程与任何第二个线程之间不存在“之前发生”的关系。这意味着第二个线程不能保证看到第一个线程对field 变量所做的更新。

简而言之,您的“改进”代码是双重检查锁定的错误实现。

【讨论】:

  • +1 双重检查锁定的损坏实现,这是一个损坏的结构。 :-)
  • @Gray - 双重检查锁定是个坏主意……但它可以正确实现……使用 Java 1.5 及更高版本。
  • 当然,但通常它比 2 次检查更复杂。 :-) 这是一个很好的例子来说明它的工作方式:stackoverflow.com/a/17112561/179850
  • @StephenC 我认为 volatile write 将充当栅栏而不是内存屏障,这里 volatile write 保证 field = (FieldType)obj 不会在 fence = true 之前使用语句重新排序。在同一个线程内。其他线程可能会看到字段的陈旧值,但进入同步块后,他们会看到最新的值
  • @veritas synchronized 已经阻止了重新排序,但 fence 如果正确使用它可以充当内存屏障,Stephen C 正确指出它不是
【解决方案2】:

在纯 JMM 中没有办法实现“便宜”的双重检查锁定;必须付出一些东西。

您的解决方案不起作用,因为可以通过以下正常操作重新排序易失性写入。请参阅jsr133 cookbook 以了解在所谓的“蟑螂汽车旅馆”模型中允许的重新排序。 “Roach motel”是比 JMM 更强大的模型,因此如果您的解决方案在 roach motel 中失败,那么它在 JMM 中也会失败。

roach motel model
reordering between a normal action and a volatile/monitor action

   --                              <--
  |                                   |
  |    VolatileLoad / MonitorEnter    | forbidden
  |                                   |
   --> allowed                      --


   --> allowed                      --
  |                                   | 
  |    VolatileStore / MonitorExit    | forbidden
  |                                   |
   --                              <--

有一种方法可以阻止“蟑螂汽车旅馆”模型中两个正常动作的重新排序

(1) action#1
(2) volatile write
(3) volatile read
(4) action#4

(1) 不能与 (2) 重新排序,(4) 不能与 (3) 重新排序,因此 (1) 和 (4) 不能重新排序。

但是,请注意,“蟑螂汽车旅馆”模型比 JMM 模型更强大。您不能确定 JVM 是否符合 roach motel 模型。举个具体的例子

action#1
synchronized(new Object()){}
synchronized(new Object()){}
action#4

根据 roach motel,action#1 和 action#4 不能重新排序;但是 JVM 可以合法地(由 JMM 允许)删除两个同步块,然后重新排序剩余的两个操作。

【讨论】:

  • 您能否提供任何参考资料,让我们可以找到更多关于 JMM 比 roach motel 模型更弱的信息
【解决方案3】:

根据我信任的消息来源,这似乎是一个有效且安全的代码。 volatile 变量的写入必须强制所有其他变量的写入,并且既不能使用 volatile 也不能使用普通赋值重新排序。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-03-08
    • 2017-03-13
    • 2014-12-04
    • 1970-01-01
    • 2012-01-11
    • 1970-01-01
    • 2016-06-12
    相关资源
    最近更新 更多