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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/3578604/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-14 02:04:48  来源:igfitidea点击:

How to solve the "Double-Checked Locking is Broken" Declaration in Java?

javamultithreadingconcurrencylockinglazy-loading

提问by monsieurBelbo

I want to implement lazy initialization for multithreading in Java.
I have some code of the sort:

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

And I'm getting the the "Double-Checked Locking is Broken" declaration.
How can I solve this?

我收到了“双重检查锁定被破坏”的声明。
我该如何解决这个问题?

采纳答案by Pascal Thivent

Here is the idiom recommended in the Item 71: Use lazy initialization judiciouslyof Effective Java:

以下是第 71 条中推荐的习惯用法:明智地使用Effective Java 的延迟初始化

If you need to use lazy initialization for performance on an instance field, use the double-check idiom. This idiom avoids the cost of locking when accessing the field after it has been initialized (Item 67). The idea behind the idiom is to check the value of the field twice (hence the name double-check): once without locking, and then, if the field appears to be uninitialized, a second time with locking. Only if the second check indicates that the field is uninitialized does the call initialize the field. Because there is no locking if the field is already initialized, it is criticalthat the field be declared volatile(Item 66). Here is the idiom:

// 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;
    }
}

This code may appear a bit convoluted. In particular, the need for the local variable result may be unclear. What this variable does is to ensure that field is read only once in the common case where it's already initialized. While not strictly necessary, this may improve performance and is more elegant by the standards applied to low-level concurrent programming. On my machine, the method above is about 25 percent faster than the obvious version without a local variable.

Prior to release 1.5, the double-check idiom did not work reliably because the semantics of the volatile modifier were not strong enough to support it [Pugh01]. The memory model introduced in release 1.5 fixed this problem [JLS, 17, Goetz06 16]. Today, the double-check idiom is the technique of choice for lazily initializing an instance field. While you can apply the double-check idiom to static fields as well, there is no reason to do so: the lazy initialization holder class idiom is a better choice.

如果您需要在实例字段上使用延迟初始化来提高性能,请使用double-check idiom。这个习惯用法避免了在初始化后访问字段时的锁定成本(条目 67)。习惯用法背后的想法是两次检查字段的值(因此名称为double-check):一次没有锁定,然后,如果该字段似乎未初始化,则第二次锁定。仅当第二次检查表明该字段未初始化时,调用才会初始化该字段。因为如果该字段已经初始化,则没有锁定,所以声明该字段至关重要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

参考

  • Effective Java, Second Edition
    • Item 71: Use lazy initialization judiciously
  • 有效的 Java,第二版
    • 第 71 条:明智地使用延迟初始化

回答by jutky

Define the variable that should be double-checked with volatilemidifier

定义应该使用volatilemidifier进行双重检查的变量

You don't need the hvariable. Here is an example from here

你不需要h变量。下面是一个例子这里

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

回答by irreputable

what do you mean, from whom you are getting the declaration?

你是什​​么意思,你从谁那里得到声明?

Double-Checked Locking is fixed. check wikipedia:

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

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;
   }

回答by erickson

Here is a pattern for correct double-checked locking.

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

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;
  }

}

For a singleton, there is a much more readable idiom for lazy initialization.

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

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

回答by samkass

The only way to do double-checked locking correctly in Java is to use "volatile" declarations on the variable in question. While that solution is correct, note that "volatile" means cache lines get flushed at every access. Since "synchronized" flushes them at the end of the block, it may not actually be any more efficient (or even less efficient). I'd recommend just not using double-checked locking unless you've profiled your code and found there to be a performance problem in this area.

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

回答by Jed Wesley-Smith

As a few have noted, you definitely need the volatilekeyword to make it work correctly, unless all members in the object are declared final, otherwise there is no happens-before pr safe-publication and you could see the default values.

正如一些人所指出的,您肯定需要volatile关键字才能使其正常工作,除非对象中的所有成员都已声明final,否则不会发生happens-before pr 安全发布并且您可以看到默认值。

We got sick of the constant problems with people getting this wrong, so we coded a LazyReferenceutility that has final semantics and has been profiled and tuned to be as fast as possible.

我们厌倦了人们经常犯的错误,因此我们编写了一个LazyReference实用程序,该实用程序具有最终语义,并且已经过分析和调整以使其尽可能快。

回答by Kanagavelu Sugumar

DCL using ThreadLocal By Brian Goetz @ JavaWorld

DCL 使用 ThreadLocal 作者:Brian Goetz @ JavaWorld

what's broken about DCL?

DCL 有什么问题?

DCL relies on an unsynchronized use of the resource field. That appears to be harmless, but it is not. To see why, imagine that thread A is inside the synchronized block, executing the statement resource = new Resource(); while thread B is just entering getResource(). Consider the effect on memory of this initialization. Memory for the new Resource object will be allocated; the constructor for Resource will be called, initializing the member fields of the new object; and the field resource of SomeClass will be assigned a reference to the newly created object.

DCL 依赖于资源字段的非同步使用。这似乎是无害的,但事实并非如此。要了解原因,假设线程 A 在同步块内,执行语句 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;
  }
}

However, since thread B is not executing inside a synchronized block, it may see these memory operations in a different order than the one thread A executes. It could be the case that B sees these events in the following order (and the compiler is also free to reorder the instructions like this): allocate memory, assign reference to resource, call constructor. Suppose thread B comes along after the memory has been allocated and the resource field is set, but before the constructor is called. It sees that resource is not null, skips the synchronized block, and returns a reference to a partially constructed Resource! Needless to say, the result is neither expected nor desired.

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

Can ThreadLocal help fix DCL?

ThreadLocal 可以帮助修复 DCL 吗?

We can use ThreadLocal to achieve the DCL idiom's explicit goal -- lazy initialization without synchronization on the common code path. Consider this (thread-safe) version of DCL:

我们可以使用 ThreadLocal 来实现 DCL 惯用法的显式目标——在公共代码路径上不同步的延迟初始化。考虑这个(线程安全)版本的 DCL:

Listing 2. DCL using ThreadLocal

清单 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;
  }
}

I think; here each thread will once enters the SYNC block to update the threadLocal value; then it will not. So ThreadLocal DCL will ensure a thread will enter only once inside the SYNC block.

我认为; 这里每个线程都会一次进入 SYNC 块来更新 threadLocal 值;那么它不会。所以 ThreadLocal DCL 将确保一个线程在 SYNC 块中只进入一次。

What does synchronized really mean?

同步的真正含义是什么?

Java treats each thread as if it runs on its own processor with its own local memory, each talking to and synchronizing with a shared main memory. Even on a single-processor system, that model makes sense because of the effects of memory caches and the use of processor registers to store variables. When a thread modifies a location in its local memory, that modification should eventually show up in the main memory as well, and the JMM defines the rules for when the JVM must transfer data between local and main memory. The Java architects realized that an overly restrictive memory model would seriously undermine program performance. They attempted to craft a memory model that would allow programs to perform well on modern computer hardware while still providing guarantees that would allow threads to interact in predictable ways.

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

Java's primary tool for rendering interactions between threads predictably is the synchronized keyword. Many programmers think of synchronized strictly in terms of enforcing a mutual exclusion semaphore (mutex) to prevent execution of critical sections by more than one thread at a time. Unfortunately, that intuition does not fully describe what synchronized means.

Java 用于可预测地呈现线程间交互的主要工具是 synchronized 关键字。许多程序员严格按照强制互斥信号量 (mutex) 来防止同时由多个线程执行临界区来考虑同步。不幸的是,这种直觉并没有完全描述同步的含义。

The semantics of synchronized do indeed include mutual exclusion of execution based on the status of a semaphore, but they also include rules about the synchronizing thread's interaction with main memory. In particular, the acquisition or release of a lock triggers a memory barrier -- a forced synchronization between the thread's local memory and main memory. (Some processors -- like the Alpha -- have explicit machine instructions for performing memory barriers.) When a thread exits a synchronized block, it performs a write barrier -- it must flush out any variables modified in that block to main memory before releasing the lock. Similarly, when entering a synchronized block, it performs a read barrier -- it is as if the local memory has been invalidated, and it must fetch any variables that will be referenced in the block from main memory.

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

回答by Animesh Chhabra

Copying below from somewhere else ,which explains why using a method local variable as a copy for the volatile variable will speed things up.

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

Statement that needs explanation:

需要说明的语句:

This code may appear a bit convoluted. In particular, the need for the local variable result may be unclear.

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

Explanation:

解释:

The field would be read first time in the first if statement and second time in the return statement. The field is declared volatile, which means it has to be refetched from memory every time it is accessed (roughly speaking, even more processing might be required to access volatile variables) and can not be stored into a register by the compiler. When copied to the local variable and then used in both statements (if and return), the register optimization can be done by the JVM.

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

回答by Branqueira

If I'm not mistaken, there is also another solution if we do not want to use the volatile keyword

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

for example by taking the previous example

例如通过前面的例子

    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;
        }
     }

the test is always on the helper variable but the construction of the object is done just before with the newHelper, it avoids to have a partially constructed object

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