Singleton mode double check lock problem

The singleton creation pattern is a common programming idiom. When used with multiple threads, some type of synchronization is required. In an effort to create more efficient code, Java programmers created the double-checked locking idiom to use with the singleton creation pattern to limit the amount of synchronization code. However, due to some less common details of the Java memory model, there is no guarantee that this double-checked locking idiom will work.

It fails occasionally, not always. Also, it fails for non-obvious reasons and contains some cryptic details of the Java memory model. These facts will cause the code to fail because double-checked locking is difficult to track. In the remainder of this article, we'll detail the double-checked locking idiom to understand where it fails.


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


Listing 1. The singleton creation idiom

copy code
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
  }
}
copy code

 

The design of this class ensures that only one  Singleton object is created. Constructors are declared as  private, getInstance() methods just create an object. This implementation is suitable for single-threaded programs. However, when multithreading is introduced,  getInstance() methods must be protected by synchronization. If the method is not protected  getInstance() , Singleton two different instances of the object may be returned. Suppose two threads call  getInstance() methods concurrently and the calls are performed in the following order:

  1. Thread 1 calls  getInstance() the method and decides  instance to be at //1  null

  2. Thread 1 entered  if the code block, but was preempted by thread 2 while executing the line of code at //2. 

  3. Thread 2 calls  the method and  decides  getInstance() at //1  instancenull

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

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

  6. Thread 2 is preempted by Thread 1. 

  7. Thread 1 starts where it left off, and executes the //2 line of code, which causes another  Singleton object to be created. 

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

The result is that  getInstance() the method creates two  Singleton objects when it should have only created one. This problem is corrected by synchronizing  getInstance() methods so that only one thread is allowed to execute 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
}

 

getInstance() The code in Listing 2 works fine for multithreaded access  methods. However, when analyzing this code, you realize that synchronization is only required the first time the method is called. Since only the first call executes the code at //2, and only this line of code needs to be synchronized, there is no need to use synchronization on subsequent calls. All other calls are used to determine  instance true and false  null , and return it. Multiple threads are able to safely execute all calls concurrently except the first. However, since this method is a method synchronized , you need to pay the price of synchronization for every call of this method, even if only the first call needs to be synchronized.

To make this approach more efficient, an idiom called double-checked locking was developed. The idea is to avoid the expensive cost of synchronizing all but the first call. The cost of synchronization varies between different JVMs. In the early days, the price was quite high. With the advent of more advanced JVMs, the cost of synchronization has decreased, but synchronized there is still a performance penalty for entering and exiting methods or blocks. Regardless of advances in JVM technology, programmers never want to waste processing time unnecessarily.

Since only the //2 line in Listing 2 needs to be synchronized, we can just wrap it in a synchronized block, as shown in Listing 3:


Listing 3. getInstance() method

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

 

The code in Listing 3 demonstrates the same problem as Listing 1, illustrated with multithreading. When  instance true null , two threads can enter  if the statement concurrently. Then, one thread enters  synchronized the block to initialize  instancewhile the other thread is blocked. When the first thread exits  synchronized the block, the waiting thread enters and creates another  Singleton object. Note: When the second thread enters  synchronized the block, it does not check  instance for negation  null.

 

double check locking

To deal with the problem in Listing 3, we need to do  instance a second check of . This is where the name "double-checked locking" comes from. Listing 4 is the result of applying the double-checked locking idiom to Listing 3 .


Listing 4. Double-checked locking example

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

 

The theory behind double-checked locking is that the second check at //2 makes it impossible (as in Listing 3) to create two different  Singleton objects. Assume the following sequence of events:

  1. Thread 1 enters  getInstance() the method. 

  2. Thread 1 enters the block at //1  because  instance of   . nullsynchronized

  3. Thread 1 is preempted by Thread 2.

  4. Thread 2 enters  getInstance() the method.

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

  6. Thread 2 is preempted by Thread 1.

  7. Thread 1 executes, and since the instance is still at //2  null, thread 1 also creates an  Singleton object and assigns its reference to  instance.

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

  9. Thread 1 is preempted by Thread 2.

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

  11. Since  instance no  null , the second  Singleton object is not created, and the object created by thread 1 is returned.

The theory behind double checked locking is perfect. Unfortunately, the reality is quite different. The problem with double-checked locking is that there is no guarantee that it will run smoothly on a uniprocessor or multiprocessor computer.

The failure of double-checked locking is not due to an implementation bug in the JVM, but to the Java platform memory model. The memory model allows for so-called "out-of-order writes", and this is a major reason why these idioms fail.

 

write out of order

To explain this, the line //3 in Listing 4 above needs to be revisited. This line of code creates an  Singleton object and initializes variables  instance to reference this object. The problem with this line of code is that  the variable  may become negated  Singleton before the constructor body is executed   .instancenull

What? This statement may surprise you, but it is true. Before explaining how this phenomenon occurs, please temporarily accept this fact, let's first examine how double-checked locking is broken. Suppose the code in Listing 4 executes the following sequence of events:

  1. Thread 1 enters  getInstance() the method.

  2. Thread 1 enters the block at //1  because  instance of   . nullsynchronized

  3. Thread 1 advances to //3, but before the constructor executes , negates the instance  null

  4. Thread 1 is preempted by Thread 2.

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

  6. Thread 2 is preempted by Thread 1.

  7. Thread 1 completes the initialization of the object by running  Singleton the object's constructor and returning a reference to it.

This sequence of events occurs when thread 2 returns an object whose constructor has not yet been executed.

To show this happening, assume  instance =new Singleton(); the following pseudocode is executed for the code 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 pseudocode is not only possible, but actually happens with some JIT compilers. The order of execution is reversed, but given the current memory model, this is allowed to happen. This behavior of the JIT compiler makes the problem of double-checked locking nothing more than an academic exercise.

To illustrate this, assume the code in Listing 5. It contains a stripped version of  getInstance() the method. I've removed the "double-checking" to simplify our review of the generated assembly code (Listing 6). We only care about how the JIT compiler compiles  instance=new Singleton(); the code. Additionally, I provide a simple constructor to explicitly illustrate how that constructor works in assembly code.


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

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

 

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


Listing 6. Assembly code generated from the code in Listing 5

copy code
;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
copy code

 

NOTE:  To refer to the lines of assembly code in the following instructions, I will refer to the last two values ​​of the instruction address since they both begin with  054D20 . For example, B5 rep  test eax,eax.

Assembly code is generated by running a testgetInstance() program that calls  methods in an infinite loop  . While the program is running, run the Microsoft Visual C++ debugger and attach it to the Java process representing the test program. Then, break out of execution and find the assembly code that represents that infinite loop.

B0B5 The first two lines of assembly code   at and   load  the instance reference from the memory location   into and   check it.  This corresponds to the first line of code in the method in Listing 5 . The first time this method is called, the code executes up   to  .  The code at   allocates memory for the object from the heap and stores a pointer to that block of memory in it   . The next line of code, , takes   the pointer in and stores it back in memory at  the instance reference. The result is that  it is now NOT   and references a valid   object. However, the constructor for this object has not yet run, which is exactly what breaks double-checked locking. Then,  at the line,  the pointer is dereferenced and stored into  .  The and   lines represent inlined constructors that store values   ​​and   stores to   objects. If this code   is interrupted by another thread after the execution line and before the constructor completes, the double-checked locking will fail.049388C8eaxnullgetInstance()instancenullB9BESingletoneaxC3eax049388C8instancenullSingletonC8instanceecxCAD0true5SingletonC3

Not all JIT compilers generate the above code. Some generate code such that negates only after the constructor  instance executes  null. Version 1.3 of the IBM SDK for Java technology and Sun JDK 1.3 both generate such code. However, this does not mean that double-checked locking should be used in these instances. There are some other reasons why this idiom fails. Also, you don't always know which JVMs your code will run on, and JIT compilers can always change to generate code that breaks this idiom.

 

Double-checked locking: acquires two

Given that the current double-checked locking doesn't work, I included another version of the code, shown in Listing 7, to prevent the out-of-order write problem you just saw.


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

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

 

Looking at the code in Listing 7, you should realize that things are getting a little ridiculous. Remember that double checked locking was created to avoid synchronization on simple three line  getInstance() methods. The code in Listing 7 becomes unruly. Also, that code doesn't solve the problem. Careful inspection will reveal the reason.

This code tries to avoid the out-of-order write problem.  It tries to solve this by introducing local variables  inst and a second  block. synchronizedThe theory is implemented as follows:

  1. Thread 1 enters  getInstance() the method.

  2. Thread 1 enters the first block at //1  because  instance of   . nullsynchronized

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

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

  5. Thread 1 then starts executing the code at //4 while negating  inst ,  nullbut  Singleton before the constructor of //4 is executed. (This is the out-of-order write problem we just saw.) 

  6. Thread 1 is preempted by Thread 2.

  7. Thread 2 enters  getInstance() the method.

  8. Because  instance of  null, thread 2 tries to enter the first  synchronized block at //1. Since thread 1 currently holds the lock, thread 2 is blocked.

  9. Thread 1 then completes execution at //4.

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

  11. Thread 1 returns  instance.

  12. instance Then execute thread 2 and assign to  at //2  inst.

  13. Thread 2 finds out  instance that it is not  null, and returns it.

The key line here is //5. This line should ensure  instance that only  null one complete  Singleton object is constructed for or referenced. The problem occurs when theory and practice run counter to each other.

The code in Listing 7 is invalid due to the definition of the current memory model. The Java Language Specification ( JLS) mandates  synchronizedthat code within a block cannot be moved out. synchronized However, it doesn't say that code outside the block  cannot be  moved synchronized into the block.

The JIT compiler will see an optimization opportunity here. This optimization removes the code at //4 and //5, combining and generating the code shown in Listing 8.


Listing 8. The optimized code from Listing 7.

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

 

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

 

How about declaring every variable volatile?

Another idea is to target variables  inst as well as  instance use keywords  volatile. According to the JLS (see Related topics), declared  volatile variables are considered sequentially consistent, ie, not reordered. But trying to  volatile fix the problem of double-checked locking with the following two problems:

  • The problem here is not about sequential consistency, but that the code was moved, not reordered.

  • Even when sequential consistency is considered, most JVMs do not implement it correctly  volatile.

The second point deserves further discussion. Assume the code in Listing 9:


Listing 9. Sequential consistency using volatile

copy code
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!
  }
  //...
}
copy code

 

According to the JLS, since  stop and  num are declared as  volatile, they should be sequentially consistent. This means that if  stop it ever was  true, num it must have been set to  100. However, because of sequential consistency features that many JVMs do not implement  volatile , you cannot rely on this behavior. Therefore, if thread 1 calls  foo and thread 2 calls concurrently  bar, thread 1 may   set to   before  num it is set to  . This will cause the thread to see   yes  while   still being set to  .  There are additional issues with using  atomic numbers with 64-bit variables, but these are beyond the scope of this article. See Resources for more information on this topic.100stoptruestoptruenum0volatile

 

solution

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

  • Accept the method shown in Listing 2  getInstance() for synchronization.

  • Abandon synchronization and use a  static field instead.

Option 2 is shown in Listing 10


Listing 10. Singleton implementation using static fields

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

 

The code in Listing 10 does not use synchronization and ensures that  static getInstance() the method is not created until it is  called Singleton. This is a great option if your goal is to eliminate syncing.

 

String is not immutable

Given the problem of out-of-order writes and references becoming negated before the constructor executes  null , you might consider  String classes. Suppose you have the following code:

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

 

String Classes should be immutable. Still, given the out-of-order write problem we discussed earlier, would that cause a problem here? The answer is yes. Consider two thread accesses String str. A thread can see  str a reference to an  String object in which the constructor has not yet run. In fact, Listing 11 contains code that shows this happening. Note that this code only fails on the older JVM I tested it with. Both IBM 1.3 and Sun 1.3 JVMs generate unchanged as expected  String.


Listing 11. Example of mutable String

copy code
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
  }
}
copy code

 

This code creates a  MutableString class at //4 that contains a  String reference shared by the two threads at //3. StringCreator At lines //5 and //6, two objects and  are created on two separate threads  StringReader. Pass in a reference to an  MutableString object. StringCreator The class enters an infinite loop and creates  String the object at //1 with the value "hello". StringReader Also enters an infinite loop, and checks at //2  String whether the value of the current object is "hello". If not, StringReader the thread prints a message and stops. If  String the class is immutable, you should see no output from this program. If an out-of-order write problem occurs, the only way to  StringReader see  str a reference is never  String an object with a value of "hello".

Running this code on an older JVM such as Sun JDK 1.2.1 will cause out-of-order write issues. and thus result in a non-invariant  String.

 

conclusion

To avoid costly synchronization in singletons, programmers were very clever and invented the double-checked locking idiom. Unfortunately, given the current memory model, this idiom has not yet become widely used, and it is clearly an unsafe programming construct. Work in this area of ​​redefining the fragile memory model is ongoing. Still, even in the newly proposed memory model, double-checked locking is ineffective. The best solution to this problem is to accept synchronization or use one  static field.

 

References

The singleton creation pattern is a common programming idiom. When used with multiple threads, some type of synchronization is required. In an effort to create more efficient code, Java programmers created the double-checked locking idiom to use with the singleton creation pattern to limit the amount of synchronization code. However, due to some less common details of the Java memory model, there is no guarantee that this double-checked locking idiom will work.

It fails occasionally, not always. Also, it fails for non-obvious reasons and contains some cryptic details of the Java memory model. These facts will cause the code to fail because double-checked locking is difficult to track. In the remainder of this article, we'll detail the double-checked locking idiom to understand where it fails.


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


Listing 1. The singleton creation idiom

copy code
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
  }
}
copy code

 

The design of this class ensures that only one  Singleton object is created. Constructors are declared as  private, getInstance() methods just create an object. This implementation is suitable for single-threaded programs. However, when multithreading is introduced,  getInstance() methods must be protected by synchronization. If the method is not protected  getInstance() , Singleton two different instances of the object may be returned. Suppose two threads call  getInstance() methods concurrently and the calls are performed in the following order:

  1. Thread 1 calls  getInstance() the method and decides  instance to be at //1  null

  2. Thread 1 entered  if the code block, but was preempted by thread 2 while executing the line of code at //2. 

  3. Thread 2 calls  the method and  decides  getInstance() at //1  instancenull

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

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

  6. Thread 2 is preempted by Thread 1. 

  7. Thread 1 starts where it left off, and executes the //2 line of code, which causes another  Singleton object to be created. 

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

The result is that  getInstance() the method creates two  Singleton objects when it should have only created one. This problem is corrected by synchronizing  getInstance() methods so that only one thread is allowed to execute 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
}

 

getInstance() The code in Listing 2 works fine for multithreaded access  methods. However, when analyzing this code, you realize that synchronization is only required the first time the method is called. Since only the first call executes the code at //2, and only this line of code needs to be synchronized, there is no need to use synchronization on subsequent calls. All other calls are used to determine  instance true and false  null , and return it. Multiple threads are able to safely execute all calls concurrently except the first. However, since this method is a method synchronized , you need to pay the price of synchronization for every call of this method, even if only the first call needs to be synchronized.

To make this approach more efficient, an idiom called double-checked locking was developed. The idea is to avoid the expensive cost of synchronizing all but the first call. The cost of synchronization varies between different JVMs. In the early days, the price was quite high. With the advent of more advanced JVMs, the cost of synchronization has decreased, but synchronized there is still a performance penalty for entering and exiting methods or blocks. Regardless of advances in JVM technology, programmers never want to waste processing time unnecessarily.

Since only the //2 line in Listing 2 needs to be synchronized, we can just wrap it in a synchronized block, as shown in Listing 3:


Listing 3. getInstance() method

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

 

The code in Listing 3 demonstrates the same problem as Listing 1, illustrated with multithreading. When  instance true null , two threads can enter  if the statement concurrently. Then, one thread enters  synchronized the block to initialize  instancewhile the other thread is blocked. When the first thread exits  synchronized the block, the waiting thread enters and creates another  Singleton object. Note: When the second thread enters  synchronized the block, it does not check  instance for negation  null.

 

double check locking

To deal with the problem in Listing 3, we need to do  instance a second check of . This is where the name "double-checked locking" comes from. Listing 4 is the result of applying the double-checked locking idiom to Listing 3 .


Listing 4. Double-checked locking example

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

 

The theory behind double-checked locking is that the second check at //2 makes it impossible (as in Listing 3) to create two different  Singleton objects. Assume the following sequence of events:

  1. Thread 1 enters  getInstance() the method. 

  2. Thread 1 enters the block at //1  because  instance of   . nullsynchronized

  3. Thread 1 is preempted by Thread 2.

  4. Thread 2 enters  getInstance() the method.

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

  6. Thread 2 is preempted by Thread 1.

  7. Thread 1 executes, and since the instance is still at //2  null, thread 1 also creates an  Singleton object and assigns its reference to  instance.

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

  9. Thread 1 is preempted by Thread 2.

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

  11. Since  instance no  null , the second  Singleton object is not created, and the object created by thread 1 is returned.

The theory behind double checked locking is perfect. Unfortunately, the reality is quite different. The problem with double-checked locking is that there is no guarantee that it will run smoothly on a uniprocessor or multiprocessor computer.

The failure of double-checked locking is not due to an implementation bug in the JVM, but to the Java platform memory model. The memory model allows for so-called "out-of-order writes", and this is a major reason why these idioms fail.

 

write out of order

To explain this, the line //3 in Listing 4 above needs to be revisited. This line of code creates an  Singleton object and initializes variables  instance to reference this object. The problem with this line of code is that  the variable  may become negated  Singleton before the constructor body is executed   .instancenull

What? This statement may surprise you, but it is true. Before explaining how this phenomenon occurs, please temporarily accept this fact, let's first examine how double-checked locking is broken. Suppose the code in Listing 4 executes the following sequence of events:

  1. Thread 1 enters  getInstance() the method.

  2. Thread 1 enters the block at //1  because  instance of   . nullsynchronized

  3. Thread 1 advances to //3, but before the constructor executes , negates the instance  null

  4. Thread 1 is preempted by Thread 2.

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

  6. Thread 2 is preempted by Thread 1.

  7. Thread 1 completes the initialization of the object by running  Singleton the object's constructor and returning a reference to it.

This sequence of events occurs when thread 2 returns an object whose constructor has not yet been executed.

To show this happening, assume  instance =new Singleton(); the following pseudocode is executed for the code 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 pseudocode is not only possible, but actually happens with some JIT compilers. The order of execution is reversed, but given the current memory model, this is allowed to happen. This behavior of the JIT compiler makes the problem of double-checked locking nothing more than an academic exercise.

To illustrate this, assume the code in Listing 5. It contains a stripped version of  getInstance() the method. I've removed the "double-checking" to simplify our review of the generated assembly code (Listing 6). We only care about how the JIT compiler compiles  instance=new Singleton(); the code. Additionally, I provide a simple constructor to explicitly illustrate how that constructor works in assembly code.


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

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

 

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


Listing 6. Assembly code generated from the code in Listing 5

copy code
;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
copy code

 

NOTE:  To refer to the lines of assembly code in the following instructions, I will refer to the last two values ​​of the instruction address since they both begin with  054D20 . For example, B5 rep  test eax,eax.

Assembly code is generated by running a testgetInstance() program that calls  methods in an infinite loop  . While the program is running, run the Microsoft Visual C++ debugger and attach it to the Java process representing the test program. Then, break out of execution and find the assembly code that represents that infinite loop.

B0B5 The first two lines of assembly code   at and   load  the instance reference from the memory location   into and   check it.  This corresponds to the first line of code in the method in Listing 5 . The first time this method is called, the code executes up   to  .  The code at   allocates memory for the object from the heap and stores a pointer to that block of memory in it   . The next line of code, , takes   the pointer in and stores it back in memory at  the instance reference. The result is that  it is now NOT   and references a valid   object. However, the constructor for this object has not yet run, which is exactly what breaks double-checked locking. Then,  at the line,  the pointer is dereferenced and stored into  .  The and   lines represent inlined constructors that store values   ​​and   stores to   objects. If this code   is interrupted by another thread after the execution line and before the constructor completes, the double-checked locking will fail.049388C8eaxnullgetInstance()instancenullB9BESingletoneaxC3eax049388C8instancenullSingletonC8instanceecxCAD0true5SingletonC3

Not all JIT compilers generate the above code. Some generate code such that negates only after the constructor  instance executes  null. Version 1.3 of the IBM SDK for Java technology and Sun JDK 1.3 both generate such code. However, this does not mean that double-checked locking should be used in these instances. There are some other reasons why this idiom fails. Also, you don't always know which JVMs your code will run on, and JIT compilers can always change to generate code that breaks this idiom.

 

Double-checked locking: acquires two

Given that the current double-checked locking doesn't work, I included another version of the code, shown in Listing 7, to prevent the out-of-order write problem you just saw.


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

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

 

Looking at the code in Listing 7, you should realize that things are getting a little ridiculous. Remember that double checked locking was created to avoid synchronization on simple three line  getInstance() methods. The code in Listing 7 becomes unruly. Also, that code doesn't solve the problem. Careful inspection will reveal the reason.

This code tries to avoid the out-of-order write problem.  It tries to solve this by introducing local variables  inst and a second  block. synchronizedThe theory is implemented as follows:

  1. Thread 1 enters  getInstance() the method.

  2. Thread 1 enters the first block at //1  because  instance of   . nullsynchronized

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

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

  5. Thread 1 then starts executing the code at //4 while negating  inst ,  nullbut  Singleton before the constructor of //4 is executed. (This is the out-of-order write problem we just saw.) 

  6. Thread 1 is preempted by Thread 2.

  7. Thread 2 enters  getInstance() the method.

  8. Because  instance of  null, thread 2 tries to enter the first  synchronized block at //1. Since thread 1 currently holds the lock, thread 2 is blocked.

  9. Thread 1 then completes execution at //4.

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

  11. Thread 1 returns  instance.

  12. instance Then execute thread 2 and assign to  at //2  inst.

  13. Thread 2 finds out  instance that it is not  null, and returns it.

The key line here is //5. This line should ensure  instance that only  null one complete  Singleton object is constructed for or referenced. The problem occurs when theory and practice run counter to each other.

The code in Listing 7 is invalid due to the definition of the current memory model. The Java Language Specification ( JLS) mandates  synchronizedthat code within a block cannot be moved out. synchronized However, it doesn't say that code outside the block  cannot be  moved synchronized into the block.

The JIT compiler will see an optimization opportunity here. This optimization removes the code at //4 and //5, combining and generating the code shown in Listing 8.


Listing 8. The optimized code from Listing 7.

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

 

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

 

How about declaring every variable volatile?

Another idea is to target variables  inst as well as  instance use keywords  volatile. According to the JLS (see Related topics), declared  volatile variables are considered sequentially consistent, ie, not reordered. But trying to  volatile fix the problem of double-checked locking with the following two problems:

  • The problem here is not about sequential consistency, but that the code was moved, not reordered.

  • Even when sequential consistency is considered, most JVMs do not implement it correctly  volatile.

The second point deserves further discussion. Assume the code in Listing 9:


Listing 9. Sequential consistency using volatile

copy code
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!
  }
  //...
}
copy code

 

According to the JLS, since  stop and  num are declared as  volatile, they should be sequentially consistent. This means that if  stop it ever was  true, num it must have been set to  100. However, because of sequential consistency features that many JVMs do not implement  volatile , you cannot rely on this behavior. Therefore, if thread 1 calls  foo and thread 2 calls concurrently  bar, thread 1 may   set to   before  num it is set to  . This will cause the thread to see   yes  while   still being set to  .  There are additional issues with using  atomic numbers with 64-bit variables, but these are beyond the scope of this article. See Resources for more information on this topic.100stoptruestoptruenum0volatile

 

solution

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

  • Accept the method shown in Listing 2  getInstance() for synchronization.

  • Abandon synchronization and use a  static field instead.

Option 2 is shown in Listing 10


Listing 10. Singleton implementation using static fields

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

 

The code in Listing 10 does not use synchronization and ensures that  static getInstance() the method is not created until it is  called Singleton. This is a great option if your goal is to eliminate syncing.

 

String is not immutable

Given the problem of out-of-order writes and references becoming negated before the constructor executes  null , you might consider  String classes. Suppose you have the following code:

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

 

String Classes should be immutable. Still, given the out-of-order write problem we discussed earlier, would that cause a problem here? The answer is yes. Consider two thread accesses String str. A thread can see  str a reference to an  String object in which the constructor has not yet run. In fact, Listing 11 contains code that shows this happening. Note that this code only fails on the older JVM I tested it with. Both IBM 1.3 and Sun 1.3 JVMs generate unchanged as expected  String.


Listing 11. Example of mutable String

copy code
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
  }
}
copy code

 

This code creates a  MutableString class at //4 that contains a  String reference shared by the two threads at //3. StringCreator At lines //5 and //6, two objects and  are created on two separate threads  StringReader. Pass in a reference to an  MutableString object. StringCreator The class enters an infinite loop and creates  String the object at //1 with the value "hello". StringReader Also enters an infinite loop, and checks at //2  String whether the value of the current object is "hello". If not, StringReader the thread prints a message and stops. If  String the class is immutable, you should see no output from this program. If an out-of-order write problem occurs, the only way to  StringReader see  str a reference is never  String an object with a value of "hello".

Running this code on an older JVM such as Sun JDK 1.2.1 will cause out-of-order write issues. and thus result in a non-invariant  String.

 

conclusion

To avoid costly synchronization in singletons, programmers were very clever and invented the double-checked locking idiom. Unfortunately, given the current memory model, this idiom has not yet become widely used, and it is clearly an unsafe programming construct. Work in this area of ​​redefining the fragile memory model is ongoing. Still, even in the newly proposed memory model, double-checked locking is ineffective. The best solution to this problem is to accept synchronization or use one  static field.

 

References

Guess you like

Origin blog.csdn.net/qq_34507736/article/details/60598737