DCL (Double-Checked Locking) in singleton mode

First of all, in order to check the issues related to instruction rearrangement in the DCL singleton mode, I found this joint statement on the DCL singleton mode, which is quite authoritative.

Signed by: David Bacon (IBM Research) Joshua Bloch (Javasoft), Jeff Bogda, Cliff Click (Hotspot JVM project), Paul Haahr, Doug Lea, Tom May, Jan-Willem Maessen, Jeremy Manson, John D. Mitchell (jGuru) Kelvin Nilsen, Bill Pugh, Emin Gun Sirer

Joint statement article address: The “Double-Checked Locking is Broken” Declaration

All analysis in this article is based on the lazy model. Double-Checked Locking will be represented by the abbreviation DCL in the follow-up.

The DCL module is widely cited and used as an efficient way to implement lazy initialization in a multi-threaded environment.

Unfortunately, when implemented in Java it doesn't work reliably in a platform-independent manner without additional synchronization, and when implemented in other languages, such as C++, it depends on the memory model of the processor, the reordering performed by the compiler, and the interaction between the compiler and the synchronization library. Since it's not specified in a language like C++, it's hard to be specific about the circumstances under which it would work. It is possible to use explicit memory barriers to make it work in C++, but these are not available in java.

First example (single-threaded version):

// Single threaded version
class Foo {
    
     
  private Helper helper = null;
  public Helper getHelper() {
    
    
    if (helper == null) 
        helper = new Helper();
    return helper;
    }
  // other functions and members...
  }

First of all, this example is a single-threaded version, and you may be fired if you write this way during the development process. Because using this code in a multi-threaded environment, it will cause many problems. The most obvious is that two or more Helper objects may be allocated. Of course, the solution is also very simple. You only need to synchronize the getHelper() method:

// Correct multithreaded version
class Foo {
    
     
  private Helper helper = null;
  public synchronized Helper getHelper() {
    
    
    if (helper == null) 
        helper = new Helper();
    return helper;
    }
  // other functions and members...
  }

Then there is this correct multi-threaded version, and the familiar synchronized keyword is added to the getHelper() method.

But every time the above code gets the Helper object, it will perform a synchronous operation. Synchronized is implemented using a monitor. Each object in java is associated with a monitor, and threads can lock and unlock it. Although these do not need to be executed explicitly, each lock and unlock still consumes resources. When the helper attribute is not null, there is no need for synchronization. For optimization, the DCL mode appears:

// Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo {
    
     
  private Helper helper = null;
  public Helper getHelper() {
    
    
    if (helper == null) 
      synchronized(this) {
    
    
        if (helper == null) 
          helper = new Helper();
      }    
    return helper;
    }
  // other functions and members...
  }

Unfortunately, this code doesn't work as we would like, nor does it work properly with compiler optimizations or in-memory multiprocessors.

In DCL mode, if the volatile keyword is not used, then helper = new Helper(); this line of code is not an atomic operation, it can be divided into the following three steps:

1. Allocate memory space

2. Initialize the object

3. Point the object to the newly allocated memory space

Due to compiler optimizations (collectively called compiler optimizations first), reordering of steps 2 and 3 may occur, and in some cases, other threads may see an object that has allocated memory but has not yet been initialized.

Given the above explanations, many suggested the following code:

// (Still) Broken multithreaded version
// "Double-Checked Locking" idiom
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...
  }

This code puts the construction of the Helper object in an inner synchronized block. The intuitive idea is that there should be a memory barrier on release synchronization, which should prevent initialization of Helper objects and assignments to helper fields from being reordered.

Unfortunately, this intuition is wrong. Synchronization rules don't work that way. The rule for monitorexit is that an action must be performed before monitorexit before the monitor can be released. However, there is no rule that operations after monitorexit cannot be completed before the monitor is released. It is perfectly reasonable and legal for the compiler to move the assignment helper=h inside a synchronized block.

So far there is no good solution to solve this DCL singleton mode, so there is a solution to save the country with a curve, which is to use the hungry man mode

class HelperSingleton {
    
    
  static Helper singleton = new Helper();
  }

This mode can certainly solve the problem, but it is not applicable to all scenarios. If we do not know whether this class is needed at the beginning, then this solution does not allow us to write programs better.

So in java, a broken one that uses ThreadLocal to solve the DCL singleton mode is derived.

class Foo {
    
    
	 /** If perThreadInstance.get() returns a non-null value, this thread
		has done synchronization needed to see initialization
		of helper */
         private final ThreadLocal perThreadInstance = new ThreadLocal();
         private Helper helper = null;
         public Helper getHelper() {
    
    
             if (perThreadInstance.get() == null) createHelper();
             return helper;
         }
         private final void createHelper() {
    
    
             synchronized(this) {
    
    
                 if (helper == null)
                     helper = new Helper();
             }
	     // Any non-null value would do as the argument here
             perThreadInstance.set(perThreadInstance);
         }
	}

The performance of this technique depends heavily on the JDK implementation you have, in Sun's 1.2 implementation, ThreadLocal performs very slowly. They are significantly faster in 1.3 and are expected to be faster in 1.4.

However, in JDK5, it brings a perfect solution, the volatile keyword.

// Works with acquire/release semantics for volatile
// Broken under current semantics for volatile
  class Foo {
    
    
        private volatile Helper helper = null;
        public Helper getHelper() {
    
    
            if (helper == null) {
    
    
                synchronized(this) {
    
    
                    if (helper == null)
                        helper = new Helper();
                }
            }
            return helper;
        }
    }

JDK5 and later extend the semantics of volatile so that the system will not allow a write of a volatile to be reordered relative to any previous read or write, and a read of a volatile cannot be reordered relative to any subsequent read or write. With this change, the DCL can work normally by modifying the helper field with volatile. But this code will not work on JDK4 or earlier.

Take a chestnut:

In the above code, the helper field is modified with the volatile keyword, then when executing the line of code helper = new Helper();

1. Allocate memory space (new keyword)

2. Initialize the Helper object

3. Establish a connection between the object and the memory space

These three points are ordered and will not reorder instructions

ok, the appetizers are ready. If it's just a simple interview to answer questions, this is enough.

Here comes the key

Double-Checked Locking Immutable Objects

If Helper is an immutable object, such that all of the fields of Helper are final, then double-checked locking will work without having to use volatile fields. The idea is that a reference to an immutable object (such as a String or an Integer) should behave in much the same way as an int or float; reading and writing references to immutable objects are atomic.

Yes, if Helper is an immutable object, i.e. all fields of Helper are final, then double-checked locking (Double-Checked Locking) will work fine without using volatile fields. The idea is that references to immutable objects such as String or Integer should behave similarly to ints and floats, and reading and writing references to immutable objects is atomic.

Small minds with big questions...

That is to say, if all properties of a class are decorated with the final keyword, then the instance of this class is immutable, that is, its state cannot be modified. In this case, the DCL can omit the volatile keyword.

The final keyword certainly cannot prevent instruction rearrangement. Its role is to ensure that variables are not modified after initialization. However, the final keyword can help us avoid problems caused by instruction rearrangement.

So I wrote the following example:

public class Singleton {
    
    
    private static Singleton instance;
    private final int value;

    private Singleton(int value) {
    
    
        this.value = value;
    }

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

I read a lot of information and found the semantics of final, which is described like this

In Java, there is a "happens-before" relationship between the writing of the final field and the end of the constructor. This means that when the constructor execution ends, all final fields have been properly initialized and are visible to other threads.

In the example of the Singleton class we gave earlier, the value attribute is decorated with the final keyword. This means that when the Singleton object is created, the value property is correctly initialized to 42. Since the writing of the final field has a happens-before relationship with the end of the constructor, when the execution of the constructor ends, the value property has been correctly initialized and is visible to other threads.

Therefore, in the example of the Singleton class we gave earlier, even if instruction rearrangement occurs, other threads will not return to see an uninitialized Singleton object. Because when other threads see that the instance variable is not null, the Singleton object has been correctly initialized, and its value property has been correctly set to 42.

Guess you like

Origin blog.csdn.net/Tanganling/article/details/130850628