如何解决Java中的“双重检查已破坏”声明?

问题描述 投票:32回答:9

我想在Java中实现多线程的延迟初始化。 我有一些类似的代码:

class Foo {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            Helper h;
            synchronized(this) {
                h = helper;
                if (h == null) 
                    synchronized (this) {
                        h = new Helper();
                    } // release inner synchronization lock
                helper = h;
            } 
        }    
        return helper;
    }
    // other functions and members...
}

而且我得到了“Double-Checked Locking is Broken”声明。 我怎么解决这个问题?

java multithreading concurrency locking lazy-loading
9个回答
71
投票

这是项目71中推荐的习语:明智地使用有效Java的懒惰初始化:

如果需要在实例字段上使用延迟初始化来提高性能,请使用双重检查惯用法。这个习惯用法避免了在初始化之后访问字段时的锁定成本(第67项)。成语背后的想法是检查字段的值两次(因此名称仔细检查):一次没有锁定,然后,如果字段看起来未初始化,则第二次锁定。仅当第二次检查表明该字段未初始化时,该呼叫才会初始化该字段。因为如果字段已经初始化,则没有锁定,因此将字段声明为volatile(第66项)至关重要。这是成语:

// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;

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

此代码可能看起来有点复杂。特别是,对局部变量结果的需求可能不清楚。这个变量的作用是确保该字段在已经初始化的常见情况下只读一次。虽然不是绝对必要,但这可以提高性能,并且通过应用于低级并发编程的标准更加优雅。在我的机器上,上面的方法比没有局部变量的明显版本快25%。

在1.5版之前,双重检查成语无法可靠地工作,因为volatile修饰符的语义不足以支持它[Pugh01]。 1.5版中引入的内存模型解决了这个问题[JLS,17,Goetz06 16]。今天,双重检查成语是懒惰初始化实例字段的首选技术。虽然您也可以将双重检查成语应用于静态字段,但没有理由这样做:延迟初始化持有者类习惯用法是更好的选择。

Reference

  • 有效的Java,第二版 第71项:明智地使用延迟初始化

12
投票

这是一个正确的双重检查锁定的模式。

class Foo {

  private volatile HeavyWeight lazy;

  HeavyWeight getLazy() {
    HeavyWeight tmp = lazy; /* Minimize slow accesses to `volatile` member. */
    if (tmp == null) {
      synchronized (this) {
        tmp = lazy;
        if (tmp == null) 
          lazy = tmp = createHeavyWeightObject();
      }
    }
    return tmp;
  }

}

对于单例,有一个更易读的习惯用于延迟初始化。

class Singleton {
  private static class Ref {
    static final Singleton instance = new Singleton();
  }
  public static Singleton get() {
    return Ref.instance;
  }
}

3
投票

在Java中正确执行双重检查锁定的唯一方法是对有问题的变量使用“volatile”声明。虽然该解决方案是正确的,但请注意“volatile”意味着在每次访问时都会刷新缓存行。由于“同步”在块的末尾刷新它们,它实际上可能不再有效(或甚至效率更低)。我建议不要使用双重检查锁定,除非您已经分析了代码并发现此区域存在性能问题。


3
投票

DCL使用ThreadLocal由Brian Goetz @ JavaWorld

关于DCL有什么打破?

DCL依赖于资源字段的不同步使用。这似乎是无害的,但事实并非如此。为了了解原因,假设线程A在synchronized块内,执行语句resource = new Resource();而线程B只是进入getResource()。考虑这种初始化对内存的影响。将分配新Resource对象的内存;将调用Resource的构造函数,初始化新对象的成员字段;并且将为SomeClass的字段资源分配对新创建的对象的引用。

class SomeClass {
  private Resource resource = null;
  public Resource getResource() {
    if (resource == null) {
      synchronized {
        if (resource == null) 
          resource = new Resource();
      }
    }
    return resource;
  }
}

但是,由于线程B没有在同步块内执行,因此它可能以与一个线程A执行的顺序不同的顺序看到这些存储器操作。可能是B按以下顺序看到这些事件的情况(并且编译器也可以自由地重新排序这样的指令):分配内存,分配对资源的引用,调用构造函数。假设线程B在分配了内存并且设置了资源字段之后但在调用构造函数之前出现。它看到资源不为null,跳过synchronized块,并返回对部分构造的Resource的引用!不用说,结果既不是预期的也不是期望的。

ThreadLocal可以帮助修复DCL吗?

我们可以使用ThreadLocal来实现DCL习语的明确目标 - 在公共代码路径上没有同步的延迟初始化。考虑DCL的这个(线程安全)版本:

清单2.使用ThreadLocal的DCL

class ThreadLocalDCL {
  private static ThreadLocal initHolder = new ThreadLocal();
  private static Resource resource = null;
  public Resource getResource() {
    if (initHolder.get() == null) {
      synchronized {
        if (resource == null) 
          resource = new Resource();
        initHolder.set(Boolean.TRUE);
      }
    }
    return resource;
  }
}

我认为;这里每个线程都会进入SYNC块以更新threadLocal值;然后它不会。因此,ThreadLocal DCL将确保线程仅在SYNC块内输入一次。

同步到底意味着什么?

Java将每个线程视为在其自己的处理器上运行,并具有自己的本地内存,每个线程与共享主内存进行通信并同步。即使在单处理器系统上,由于内存缓存的影响以及使用处理器寄存器来存储变量,该模型也是有意义的。当线程修改其本地内存中的位置时,该修改最终也应该显示在主内存中,并且JMM定义JVM何时必须在本地内存和主内存之间传输数据的规则。 Java架构师意识到过度限制的内存模型会严重破坏程序性能。他们试图制作一种内存模型,使程序在现代计算机硬件上运行良好,同时仍然提供允许线程以可预测的方式进行交互的保证。

用于在线程之间呈现交互的Java主要工具是synchronized关键字。许多程序员认为在强制执行互斥信号量(mutex)方面严格同步,以防止一次多个线程执行关键部分。不幸的是,这种直觉并没有完全描述同步意味着什么。

synchronized的语义确实包括基于信号量的状态互斥执行,但它们还包括有关同步线程与主存储器交互的规则。特别是,锁的获取或释放会触发内存屏障 - 线程本地内存和主内存之间的强制同步。 (某些处理器 - 像Alpha一样 - 有明确的机器指令用于执行内存屏障。)当一个线程退出同步块时,它会执行写屏障 - 它必须在释放之前将该块中修改的任何变量清除到主内存中锁。类似地,当进入同步块时,它执行读屏障 - 就好像本地存储器已经无效,并且它必须从主存储器中获取将在块中引用的任何变量。


2
投票

定义应使用qazxsw po修饰符进行双重检查的变量

你不需要volatile变量。以下是h的一个例子

here

2
投票

你是什​​么意思,从谁那里得到宣言?

双重检查锁定是固定的。检查维基百科:

class Foo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null)
                    helper = new Helper();
            }
        }
        return helper;
    }
}

2
投票

正如一些人已经指出的那样,你肯定需要public class FinalWrapper<T> { public final T value; public FinalWrapper(T value) { this.value = value; } } public class Foo { private FinalWrapper<Helper> helperWrapper = null; public Helper getHelper() { FinalWrapper<Helper> wrapper = helperWrapper; if (wrapper == null) { synchronized(this) { if (helperWrapper ==null) helperWrapper = new FinalWrapper<Helper>( new Helper() ); wrapper = helperWrapper; } } return wrapper.value; } 关键字才能使它正常工作,除非对象中的所有成员都被声明为volatile,否则在安全发布之前没有发生,你可以看到默认值。

我们厌倦了人们犯这个错误的常见问题,所以我们编写了一个final实用程序,它具有最终的语义,并且已经被描述和调整为尽可能快。


2
投票

从下面的其他地方复制,这解释了为什么使用方法局部变量作为volatile变量的副本会加快速度。

需要说明的声明:

此代码可能看起来有点复杂。特别是,对局部变量结果的需求可能不清楚。

说明:

该字段将在第一个if语句中第一次读取,第二次在return语句中读取。该字段被声明为volatile,这意味着每次访问时都必须从内存中重新获取(粗略地说,访问volatile变量可能需要更多处理),并且不能由编译器存储到寄存器中。当复制到局部变量然后在两个语句中使用(if和return)时,寄存器优化可以由JVM完成。


-2
投票

如果我没有弄错的话,如果我们不想使用volatile关键字,还有另一种解决方案

例如,通过前面的例子

LazyReference

测试总是在辅助变量上,但是对象的构造就在newHelper之前完成,它避免了部分构造的对象

© www.soinside.com 2019 - 2024. All rights reserved.