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

时间:2010-08-26 19:10:12

标签: java multithreading concurrency locking lazy-loading

我想在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...
}

我正在接受“双重检查已破损”声明 我该如何解决这个问题?

9 个答案:

答案 0 :(得分:70)

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

  

如果你需要使用延迟初始化来提高性能   实例字段,使用仔细检查   成语 即可。这个成语避免了成本   在访问该字段时锁定   初始化之后(项目   67)。成语背后的想法是   检查字段的值两次   (因此名称​​仔细检查):一次   没有锁定,然后,如果   字段似乎未初始化,a   第二次锁定。只有当   第二次检查表明该字段   是未初始化的电话   初始化该字段。因为有   如果该字段已经没有锁定   初始化,它是 critical   字段声明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]。今天,   复核成语是一种技巧   懒惰地初始化的选择   实例字段。虽然你可以申请   对于静态的复核成语   领域也是如此,没有理由   这样做:懒惰的初始化持有者   成语是一个更好的选择。

参考

  • Effective Java,Second Edition
    • 第71项:明智地使用延迟初始化

答案 1 :(得分: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;
  }
}

答案 2 :(得分:3)

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

答案 3 :(得分:3)

  

DCL使用ThreadLocal By 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一样 - 有明确的机器指令用于执行内存屏障。)当一个线程退出同步块时,它会执行写屏障 - 它必须在释放之前将该块中修改的任何变量清除到主内存中锁。类似地,当进入同步块时,它执行读屏障 - 就好像本地存储器已经无效,并且它必须从主存储器中获取将在块中引用的任何变量。

答案 4 :(得分:2)

定义应使用volatile midifier

进行双重检查的变量

您不需要h变量。 以下是here

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

答案 5 :(得分: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;
   }

答案 6 :(得分:2)

正如一些人所指出的那样,你肯定需要volatile关键字来使其正常工作,除非对象中的所有成员都被声明为final,否则没有发生 - 在pr safe-publication之前你可以看到默认值。

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

答案 7 :(得分:2)

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

需要解释的声明:

  

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

说明:

  

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

答案 8 :(得分:-2)

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

例如通过前面的例子

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

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