`
daweiangel
  • 浏览: 325775 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类

双check单子模式

阅读更多
Singleton creation idiom

To understand where the double-checked locking idiom originated, you must understand the common singleton creation idiom, which is illustrated in Listing 1:

Listing 1. Singleton creation idiom

               

import java.util.*;
class Singleton
{
  private static Singleton instance;
  private Vector v;
  private boolean inUse;

  private Singleton()
  {
    v = new Vector();
    v.addElement(new Object());
    inUse = true;
  }

  public static Singleton getInstance()
  {
    if (instance == null)          //1
      instance = new Singleton();  //2
    return instance;               //3
  }
}



The design of this class ensures that only one Singleton object is ever created. The constructor is declared private and the getInstance() method creates only one object. This implementation is fine for a single-threaded program. However, when multiple threads are introduced, you must protect the getInstance() method through synchronization. If the getInstance() method is not protected, it is possible to return two different instances of the Singleton object. Consider two threads calling the getInstance() method concurrently and the following sequence of events:

   1. Thread 1 calls the getInstance() method and determines that instance is null at //1.

   2. Thread 1 enters the if block, but is preempted by thread 2 before executing the line at //2.

   3. Thread 2 calls the getInstance() method and determines that instance is null at //1.

   4. Thread 2 enters the if block and creates a new Singleton object and assigns the variable instance to this new object at //2.

   5. Thread 2 returns the Singleton object reference at //3.

   6. Thread 2 is preempted by thread 1.

   7. Thread 1 starts where it left off and executes line //2 which results in another Singleton object being created.

   8. Thread 1 returns this object at //3.

The result is that the getInstance() method created two Singleton objects when it was supposed to create only one. This problem is corrected by synchronizing the getInstance() method to allow only one thread to execute the code at a time, as shown in Listing 2:

Listing 2. Thread-safe getInstance() method

               

public static synchronized Singleton getInstance()
{
  if (instance == null)          //1
    instance = new Singleton();  //2
  return instance;               //3
}


The code in Listing 2 works fine for multithreaded access to the getInstance() method. However, when you analyze it you realize that synchronization is required only for the first invocation of the method. Subsequent invocations do not require synchronization because the first invocation is the only invocation that executes the code at //2, which is the only line that requires synchronization. All other invocations determine that instance is non-null and return it. Multiple threads can safely execute concurrently on all invocations except the first. However, because the method is synchronized, you pay the cost of synchronization for every invocation of the method, even though it is only required on the first invocation.

In an effort to make this method more efficient, an idiom called double-checked locking was created. The idea is to avoid the costly synchronization for all invocations of the method except the first. The cost of synchronization differs from JVM to JVM. In the early days, the cost could be quite high. As more advanced JVMs have emerged, the cost of synchronization has decreased, but there is still a performance penalty for entering and leaving a synchronized method or block. Regardless of the advancements in JVM technology, programmers never want to waste processing time unnecessarily.

Because only line //2 in Listing 2 requires synchronization, we could just wrap it in a synchronized block, as shown in Listing 3:

Listing 3. The getInstance() method

               

public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {
      instance = new Singleton();
    }
  }
  return instance;
}


The code in Listing 3 exhibits the same problem as demonstrated with multiple threads and Listing 1. Two threads can get inside of the if statement concurrently when instance is null. Then, one thread enters the synchronized block to initialize instance, while the other is blocked. When the first thread exits the synchronized block, the waiting thread enters and creates another Singleton object. Note that when the second thread enters the synchronized block, it does not check to see if instance is non-null.


Back to top


Double-checked locking

To fix the problem in Listing 3, we need a second check of instance. Thus, the name "double-checked locking." Applying the double-checked locking idiom to Listing 3 results in Listing 4.

Listing 4. Double-checked locking example

               

public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {  //1
      if (instance == null)          //2
        instance = new Singleton();  //3
    }
  }
  return instance;
}



The theory behind double-checked locking is that the second check at //2 makes it impossible for two different Singleton objects to be created as occurred in Listing 3. Consider the following sequence of events:

   1. Thread 1 enters the getInstance() method.

   2. Thread 1 enters the synchronized block at //1 because instance is null.

   3. Thread 1 is preempted by thread 2.

   4. Thread 2 enters the getInstance() method.

   5. Thread 2 attempts to acquire the lock at //1 because instance is still null. However, because thread 1 holds the lock, thread 2 blocks at //1.

   6. Thread 2 is preempted by thread 1.

   7. Thread 1 executes and because instance is still null at //2, creates a Singleton object and assigns its reference to instance.

   8. Thread 1 exits the synchronized block and returns instance from the getInstance() method.

   9. Thread 1 is preempted by thread 2.

  10. Thread 2 acquires the lock at //1 and checks to see if instance is null.

  11. Because instance is non-null, a second Singleton object is not created and the one created by thread 1 is returned.

The theory behind double-checked locking is perfect. Unfortunately, reality is entirely different. The problem with double-checked locking is that there is no guarantee it will work on single or multi-processor machines.

The issue of the failure of double-checked locking is not due to implementation bugs in JVMs but to the current Java platform memory model. The memory model allows what is known as "out-of-order writes" and is a prime reason why this idiom fails.


Back to top


Out-of-order writes

To illustrate the problem, you need to re-examine line //3 from Listing 4 above. This line of code creates a Singleton object and initializes the variable instance to refer to this object. The problem with this line of code is that the variable instance can become non-null before the body of the Singleton constructor executes.

Huh? That statement might be contradictory to everything you thought possible, but it is, in fact, the case. Before explaining how this happens, accept this fact for a moment while examining how this breaks the double-checked locking idiom. Consider the following sequence of events with the code in Listing 4:

   1. Thread 1 enters the getInstance() method.

   2. Thread 1 enters the synchronized block at //1 because instance is null.

   3. Thread 1 proceeds to //3 and makes instance non-null, but before the constructor executes.

   4. Thread 1 is preempted by thread 2.

   5. Thread 2 checks to see if instance is null. Because it is not, thread 2 returns the instance reference to a fully constructed, but partially initialized, Singleton object.

   6. Thread 2 is preempted by thread 1.

   7. Thread 1 completes the initialization of the Singleton object by running its constructor and returns a reference to it.

This sequence of events results in a period of time where thread 2 returned an object whose constructor had not executed.

To show how this occurs, consider the following pseudo code for the line: instance =new Singleton();

mem = allocate();             //Allocate memory for Singleton object.
instance = mem;               //Note that instance is now non-null, but
                              //has not been initialized.
ctorSingleton(instance);      //Invoke constructor for Singleton passing
                              //instance.


This pseudo code is not only possible, but is exactly what happens on some JIT compilers. The order of execution is perceived to be out of order, but is allowed to happen given the current memory model. The fact that JIT compilers do just this makes the issues of double-checked locking more than simply an academic exercise.

To demonstrate this, consider the code in Listing 5. It contains a stripped-down version of the getInstance() method. I've removed the "double-checkedness" to ease our review of the assembly code produced (Listing 6). We are interested only in seeing how the line instance=new Singleton(); is compiled by the JIT compiler. In addition, I've provided a simple constructor to make it clear when the constructor is run in the assembly code.

Listing 5. Singleton class to demonstrate out-of-order writes

               

class Singleton
{
  private static Singleton instance;
  private boolean inUse;
  private int val; 

  private Singleton()
  {
    inUse = true;
    val = 5;
  }
  public static Singleton getInstance()
  {
    if (instance == null)
      instance = new Singleton();
    return instance;
  }
}



Listing 6 contains the assembly code produced by the Sun JDK 1.2.1 JIT compiler for the body of the getInstance() method from Listing 5.

Listing 6. Assembly code produced from code in Listing 5

               

;asm code generated for getInstance
054D20B0   mov         eax,[049388C8]      ;load instance ref
054D20B5   test        eax,eax             ;test for null
054D20B7   jne         054D20D7
054D20B9   mov         eax,14C0988h
054D20BE   call        503EF8F0            ;allocate memory
054D20C3   mov         [049388C8],eax      ;store pointer in
                                           ;instance ref. instance 
                                           ;non-null and ctor
                                           ;has not run
054D20C8   mov         ecx,dword ptr [eax]
054D20CA   mov         dword ptr [ecx],1   ;inline ctor - inUse=true;
054D20D0   mov         dword ptr [ecx+4],5 ;inline ctor - val=5;
054D20D7   mov         ebx,dword ptr ds:[49388C8h]
054D20DD   jmp         054D20B0


Note: To reference the lines of assembly code in the following explanation, I refer to the last two values of the instruction address because they all begin with 054D20. For example, B5 refers to test eax,eax.

The assembly code is produced by running a test program that calls the getInstance() method in an infinite loop. While the program runs, run the Microsoft Visual C++ debugger and attach it to the Java process representing the test program. Then, break the execution and find the assembly code representing the infinite loop.

The first two lines of assembly code at B0 and B5 load the instance reference from memory location 049388C8 into eax and test for null. This corresponds to the first line of the getInstance() method in Listing 5. The first time this method is called, instance is null and the code proceeds to B9 . The code at BE allocates the memory from the heap for the Singleton object and stores a pointer to that memory in eax. The next line, C3, takes the pointer in eax and stores it back into the instance reference at memory location 049388C8. As a result, instance is now non-null and refers to a valid Singleton object. However, the constructor for this object has not run yet, which is precisely the situation that breaks double-checked locking. Then at line C8, the instance pointer is dereferenced and stored in ecx. Lines CA and D0 represent the inline constructor storing the values true and 5 into the Singleton object. If this code is interrupted by another thread after executing line C3 but before completing the constructor, double-checked locking fails.

Not all JIT compilers generate the code as above. Some generate code such that instance becomes non-null only after the constructor executes. Both the IBM SDK for Java technology, version 1.3 and the Sun JDK 1.3 produce code such as this. However, this does not mean you should use double-checked locking in these instances. There are other reasons it could fail. In addition, you do not always know which JVMs your code will run on, and the JIT compiler could always change to generate code that breaks this idiom.


Back to top


Double-checked locking: Take two

Given that the current double-checked locking code does not work, I've put together another version of the code, shown in Listing 7, to try to prevent the out-of-order write problem you just saw.

Listing 7. Attempting to solve the out-of-order write problem

               

public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {      //1
      Singleton inst = instance;         //2
      if (inst == null)
      {
        synchronized(Singleton.class) {  //3
          inst = new Singleton();        //4
        }
        instance = inst;                 //5
      }
    }
  }
  return instance;
}



Looking at the code in Listing 7 you should realize that things are getting a little ridiculous. Remember, double-checked locking was created as a way to avoid synchronizing the simple three-line getInstance() method. The code in Listing 7 has gotten out of hand. In addition, the code does not fix the problem. Careful examination reveals why.

This code is trying to avoid the out-of-order write problem. It tries to do this by introducing the local variable inst and a second synchronized block. The theory works as follows:

   1. Thread 1 enters the getInstance() method.

   2. Because instance is null, thread 1 enters the first synchronized block at //1.

   3. The local variable inst gets the value of instance, which is null at //2.

   4. Because inst is null, thread 1 enters the second synchronized block at //3.

   5. Thread 1 then begins to execute the code at //4, making inst non-null but before the constructor for Singleton executes. (This is the out-of-order write problem we just saw.)

   6. Thread 1 is preempted by Thread 2.

   7. Thread 2 enters the getInstance() method.

   8. Because instance is null, thread 2 attempts to enter the first synchronized block at //1. Because thread 1 currently holds this lock, thread 2 blocks.

   9. Thread 1 then completes its execution of //4.

  10. Thread 1 then assigns a fully constructed Singleton object to the variable instance at //5 and exits both synchronized blocks.

  11. Thread 1 returns instance.

  12. Thread 2 then executes and assigns instance to inst at //2.

  13. Thread 2 sees that instance is non-null, and returns it.

The key line here is //5. This line is supposed to ensure that instance will only ever be null or refer to a fully constructed Singleton object. The problem occurs where theory and reality run orthogonal to one another.

The code in Listing 7 doesn't work because of the current definition of the memory model. The Java Language Specification (JLS) demands that code within a synchronized block not be moved out of a synchronized block. However, it does not say that code not in a synchronized block cannot be moved into a synchronized block.

A JIT compiler would see an optimization opportunity here. This optimization would remove the code at //4 and the code at //5, combine it and generate the code shown in Listing 8:

Listing 8. Optimized code from Listing 7

               

public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {      //1
      Singleton inst = instance;         //2
      if (inst == null)
      {
        synchronized(Singleton.class) {  //3
          //inst = new Singleton();      //4
          instance = new Singleton();              
        }
        //instance = inst;               //5
      }
    }
  }
  return instance;
}



If this optimization takes place, you have the same out-of-order write problem we discussed earlier.


Back to top


volatile anyone?

Another idea is to use the keyword volatile for the variables inst and instance. According to the JLS (see Resources), variables declared volatile are supposed to be sequentially consistent, and therefore, not reordered. But two problems occur with trying to use volatile to fix the problem with double-checked locking:

    * The problem here is not with sequential consistency. Code is being moved, not reordered.

    * Many JVMs do not implement volatile correctly regarding sequential consistency anyway.

The second point is worth expanding upon. Consider the code in Listing 9:

Listing 9. Sequential consistency with volatile

               

class test
{
  private volatile boolean stop = false;
  private volatile int num = 0;

  public void foo()
  {
    num = 100;    //This can happen second
    stop = true;  //This can happen first
    //...
  }

  public void bar()
  {
    if (stop)
      num += num;  //num can == 0!
  }
  //...
}


According to the JLS, because stop and num are declared volatile, they should be sequentially consistent. This means that if stop is ever true, num must have been set to 100. However, because many JVMs do not implement the sequential consistency feature of volatile, you cannot count on this behavior. Therefore, if thread 1 called foo and thread 2 called bar concurrently, thread 1 might set stop to true before num is set to 100. This could lead thread 2 to see stop as true, but num still set to 0. There are additional problems with volatile and the atomicity of 64-bit variables, but this is beyond the scope of this article. See Resources for more information on this topic.


Back to top


The solution

The bottom line is that double-checked locking, in whatever form, should not be used because you cannot guarantee that it will work on any JVM implementation. JSR-133 is addressing issues regarding the memory model, however, double-checked locking will not be supported by the new memory model. Therefore, you have two options:

    * Accept the synchronization of a getInstance() method as shown in Listing 2.

    * Forgo synchronization and use a static field.

Option 2 is shown in Listing 10:

Listing 10. Singleton implementation with static field

               

class Singleton
{
  private Vector v;
  private boolean inUse;
  private static Singleton instance = new Singleton();

  private Singleton()
  {
    v = new Vector();
    inUse = true;
    //...
  }

  public static Singleton getInstance()
  {
    return instance;
  }
}


The code in Listing 10 does not use synchronization and ensures that the Singleton object is not created until a call is made to the static getInstance() method. This is a good alternative if your objective is to eliminate synchronization.


Back to top


String is not immutable

You might wonder about the String class given the issue of out-of-order writes and a reference becoming non-null prior to the constructor executing. Consider the following code:

private String str;
//...
str = new String("hello");


The String class is supposed to be immutable. However, given the out-of-order write problem we discussed previously, could that cause a problem here? The answer is it could. Consider two threads with access to the String str. One thread could see the str reference refer to a String object in which the constructor has not run. In fact, Listing 11 contains code that shows this occurring. Note that this code breaks only with older JVMs that I tested. Both the IBM 1.3 and Sun 1.3 JVMs produce immutable Strings as expected.

Listing 11. Example of a Mutable String

               

class StringCreator extends Thread
{
  MutableString ms;
  public StringCreator(MutableString muts)
  {
    ms = muts;
  }
  public void run()
  {
    while(true)
      ms.str = new String("hello");          //1
  }
}
class StringReader extends Thread
{
  MutableString ms;
  public StringReader(MutableString muts)
  {
    ms = muts;
  }
  public void run()
  {
    while(true)
    {
      if (!(ms.str.equals("hello")))         //2
      {
        System.out.println("String is not immutable!");
        break;
      }
    }
  }
}
class MutableString
{
  public String str;                         //3
  public static void main(String args[])
  {
    MutableString ms = new MutableString();  //4
    new StringCreator(ms).start();           //5
    new StringReader(ms).start();            //6
  }
}


This code creates a MutableString class at //4 that contains a String reference shared by two threads at //3. Two objects are created, StringCreator and StringReader, on two separate threads at lines //5 and //6, passing a reference to the MutableString object. The StringCreator class enters an infinite loop and creates String objects with the value "hello" at //1. The StringReader also enters an infinite loop and checks to see if the current String object has the value "hello" at //2. If it doesn't, the StringReader thread prints out a message and stops. If the String class is immutable, you should never see any output from this program. The only way for StringReader to see the str reference to be anything other than a String object with "hello" as its value is if the problem of out-of-order writes occurs.

Running this code on old JVMs like Sun JDK 1.2.1 results in the out-of-order write problem, and thus, a non-immutable String.
分享到:
评论

相关推荐

    cppcheck操作手册

    cppcheck操作手册 Cppcheck是C/C++代码分析工具,可以检测bugs和不安全的编程结构,目标是检测代码中的真实错误,而不是报告虚拟警告。Cppcheck可以分析非标准语法的代码,例如嵌入式项目常用的语法。 Cppcheck的...

    cppcheck规则编写文档

    cppcheck是一款静态代码分析工具,主要用于检测C/C++代码中的潜在错误、未初始化的变量、内存泄漏、冗余代码等问题。它具有高度可扩展性,允许用户根据自己的需求编写自定义规则来增强其检查能力。在cppcheck中,...

    cppcheck在sourceinsight上使用

    ### cppcheck在SourceInsight上的使用详解 #### 一、cppcheck简介与安装 cppcheck是一种广泛应用于C/C++代码的静态分析工具,它能够帮助开发者发现潜在的编程错误、编码风格问题以及不符合最佳实践的地方。cppcheck...

    Qt Creator plugin动手实践(4)修改qtc-cppcheck源码,快捷使用cppcheck

    在本实践教程中,我们将深入探索Qt Creator插件开发,特别是如何修改qtc-cppcheck插件的源码,以便更高效地利用cppcheck静态分析工具。cppcheck是一款开源的C/C++代码检查器,能帮助开发者在编译之前发现潜在的错误...

    文件检查FileCheck

    - **正则表达式**:通过使用正则表达式,可以匹配更复杂的关键字模式,如日期格式、邮箱地址等。 - **上下文匹配**:有时我们需要确保关键字出现在特定上下文中,可以结合前后文一起检查,提高检查的准确性。 - *...

    cppcheck_win10安装包.zip

    cppcheck 是一款强大的静态代码分析工具,主要用于C、C++编程语言。它可以在代码执行之前检测出潜在的错误,如未初始化的变量、空指针解引用、内存泄漏、潜在的除以零等常见问题。cppcheck_win10安装包.zip 是专为...

    dependency-check-7.1.1-release

    依赖检查工具dependency-check是软件开发领域中用于检测项目依赖库是否存在已知安全漏洞的重要工具。在版本7.1.1中,它提供了最新的安全数据库和改进的分析功能,以帮助开发者确保他们的应用程序免受潜在的安全威胁...

    Cppcheck工具手册

    * 危险的编码模式 * 编码风格 然而,静态分析也有一些局限性,例如无法检测到程序的意图和预期结果。如果程序的输出结果是有效的,但不符合预期结果,那么静态分析工具可能无法检测到这种情况。 因此,静态分析...

    WITH CHECK OPTION的用法

    在数据库管理领域,尤其是SQL语言的应用中,"WITH CHECK OPTION"是一个重要的概念,它主要用于视图的定义中,以限制通过视图进行的数据修改操作。本文将深入探讨WITH CHECK OPTION的用法及其背后的原理,帮助读者更...

    check约束的文档

    "SQL CHECK 约束详解" 在 SQL 中,CHECK 约束是一种约束类型,用于定义表中一列或多列可接受的数据值或格式。CHECK 约束可以应用于多个列,也可以将多个 CHECK 约束应用于单个列。 CHECK 约束的定义: CHECK ...

    checkflash-官方版

    而Switches.txt文件则包含了命令行参数的详细说明,用户可以根据需要调整测试模式。 在执行过程中,"checkflash"会进行多次读写操作,模拟真实环境下U盘的工作情况,从而获取准确的性能数据。测试结果通常会包括...

    CppCheck 码缺陷静态检查工具

    此外,对于某些特定的编程模式或库,它的检查可能不够全面。 总的来说,CppCheck是开发过程中不可或缺的辅助工具,通过其详尽的代码检查,开发者可以尽早发现并修复问题,提升代码质量,降低维护成本。

    cppcheck 2.10.1

    cppcheck是一款静态代码分析工具,专门用于检测C++代码中的潜在错误、未初始化的变量、内存泄漏、冗余代码等问题。2.10.1是cppcheck的一个版本号,这通常意味着它包含了先前版本的改进、新功能的添加以及可能的错误...

    cppcheck操作文档

    ### cppcheck操作文档知识点 #### 一、简介 **cppcheck**是一款专为C/C++代码设计的静态分析工具。与传统的编译器和其他分析工具不同的是,cppcheck不负责检查语法错误,而是专注于发现那些通常被编译器忽略的问题...

    cppcheck配置文件

    **cppcheck配置文件详解** cppcheck是一款开源的静态代码分析工具,主要用于C、C++语言的编程中,它能够在编译之前发现潜在的错误和问题,如未初始化的变量、空指针引用、内存泄漏、可能的空指针解引用等。在Visual...

    cppcheck source code

    - **软件工程实践**:cppcheck的代码组织结构、设计模式、单元测试等方面体现了良好的软件工程实践。 - **构建系统**:学习如何使用Makefile或CMake等工具来管理项目构建过程。 - **开源社区协作**:了解开源项目的...

    checkit3.0

    CheckIt 3.0能够帮助用户检查LPT口的物理连接,测试数据传输功能,识别端口设置,以及发现可能存在的兼容性问题。 除了COM和LPT接口的检测,CheckIt 3.0可能还具备其他硬件信息测试功能,例如: 1. **CPU信息**:...

Global site tag (gtag.js) - Google Analytics