About the singleton pattern

Singleton pattern I believe everyone is familiar with, when learning design pattern, often the first to learn is singleton pattern. There are many implementations of the singleton pattern in Java, the most common being “double lock detection,” “static inner classes,” and “enumeration.” Effective Java recommends using enumerations.

But today’s discussion is about some of the explorations and thoughts that the volatile keyword has led to when implementing singletons using “double-lock detection.” For space reasons, this article assumes that you already know the following:

  • Java memory model
  • Memory semantics for the volatile keyword
  • Synchronized memory semantics of a synchronization lock
  • Happens-before rules for volatile and synchronized synchronized locks

What’s wrong with not using volatile?

A double-lock verification singleton without volatile might look like this:

public class Singleton {

    private static Singleton instance; // Do not use volatile
    
    // Double lock check
    public static Singleton getInstance(a) {
        if (instance == null) { / / line 7
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); / / line 10}}}returninstance; }}Copy the code

What’s wrong with this code? We know that a lock is unlocked happens-before a lock is unlocked later. At first glance, the above code is not too problematic. Locking does not guarantee that code in the synchronization zone will not be reordered. For line 10, it is possible to be decomposed and reordered by the JVM, i.e. :

instance = new Singleton(); / / line 10

// This can be broken down into three steps
1 memory=allocate();// Allocate memory equivalent to c malloc
2 ctorInstanc(memory) // Initialize the object
3 s=memory // Set s to the newly assigned address

// The above three steps may be reordered to 1-3-2, i.e. :
1 memory=allocate();// Allocate memory equivalent to c malloc
3 s=memory // Set s to the newly assigned address
2 ctorInstanc(memory) // Initialize the object
Copy the code

Once such A reordering occurs, let’s say thread A performs steps 1 and 3 on line 10, but step 2 is not complete. At line 7, thread B decides that instance is not empty and returns an uninitialized instance!

How does Volatile solve this problem?

To address these issues, after Java 5, the JMM model allowed us to use the volatile keyword to prohibit such reordering. For JMM’s happens-before rule, which is a write to a volatile variable, happens-before is a subsequent read to that variable. So we can declare instance with the volatile keyword.

public class Singleton {

    private static volatile Singleton instance; // Use volatile
    
    // Double lock check
    public static Singleton getInstance(a) {
        if (instance == null) { / / line 7
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); / / line 10}}}returninstance; }}Copy the code

OK, the problem seems to be solved. But I still have a question in my mind: given that volatile was not used, would an uninitialized instance really be returned? What happens if the instance is not initialized?

What happens if you don’t use volatile?

Let’s start with a Java object instantiation process:

1. Allocate space for the object and initialize it by default based on the property type. Ps: Eight basic data types are initialized by default and other data types are initialized by default. Initialization of parent class attributes (including code blocks, and attributes initialized in code order) 3. Initialization of the parent class constructor 4. Initialization of subclass attributes (same as the parent class) 5. Initialization of the subclass constructor

Driven by curiosity, I wrote a Demo code to do an experiment:

// singleton code
public class Singleton {

    private static Singleton instance; / / without volatile

    private volatile boolean flag = false; // a flag to indicate whether the initialization is complete

    private Singleton(a) {
        try {
            Thread.sleep(1000);
            flag = true;
        } catch(InterruptedException e) { e.printStackTrace(); }}// call to the client, should return false if initialization is incomplete, and true if completed
    public boolean isFlag(a) {
        return flag;
    }

    // Double lock check implements singleton mode
    public static Singleton getInstance(a) {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = newSingleton(); }}}returninstance; }}Copy the code
// Client code
public class SingletonDemo {

    private final static int THREAD_NUMBER = 1000; // Number of threads

    private static class MyThread implements Runnable {

        @Override
        public void run(a) {
            Singleton singleton = Singleton.getInstance();
            if(! singleton.isFlag()) { System.out.println("I am false!!!"); }}}public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(newMyThread()).start(); }}}Copy the code

It is possible for a client to call isFlag() to return false if it is possible to return an instance that has not been initialized.

Magical thing happened, I adjusted various parameters (thread count and sleep time) and ran it several times, but it didn’t print “I am false!!” This sentence! In other words, there is no theoretical reordering happening there!

What is the reason? Why didn’t reorder happen?

The “Double-Checked Locking is Broken” Declaration says that if you use The Symantec JIT (a compiler that accesses objects based on a handle), The code you compile will undergo this reordering.

The author was unable to find a Symantec JIT or another compiler to access objects based on handles to experiment with. But take a look at the decompilation results for HotSpot.

Let’s decompile it using HotSpot’s Javap tool:

javac Singleton.java
javap -l -v Singleton.class
Copy the code
 public static communication.Singleton getInstance(a);
    descriptor: ()Lcommunication/Singleton;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: getstatic     #8                  // Field instance:Lcommunication/Singleton;
         3: ifnonnull     37
         6: ldc           #9                  // class communication/Singleton
         8: dup
         9: astore_0
        10: monitorenter
        11: getstatic     #8                  // Field instance:Lcommunication/Singleton;
        14: ifnonnull     27
        17: new           #9                  // class communication/Singleton
        20: dup
        21: invokespecial #10                 // Method "<init>":()V
        24: putstatic     #8                  // Field instance:Lcommunication/Singleton;
        27: aload_0
        28: monitorexit
        / / to omit
Copy the code

The sequence number 17 through 24 should be the process of new an object. Explain one by one:

  • New: Allocates memory for objects on the Java heap and pushes the address to the top of the operand stack;
  • Dup: Copies the top value of the operand stack and pushes it to the top, i.e. there are two consecutive object addresses on the operand stack
  • Invokespecial: Used to call instance methods that require special processing, including instance initialization methods, private methods, and parent methods.
  • Putstatic: The value is taken from the top of the stack and stored in a static variable
  • Aload_0: Pushes the this reference onto the operand stack
  • Monitorexit: Releases locks

As you can see, it is instantiated first and then stored in the static variable instance. In other words, there’s no reordering going on here.

conclusion

Again, let’s look at the two ways Java accesses objects: handle access and direct access.

Thinking back to the possible reordering results mentioned earlier, we might guess that only handle access is likely to have that kind of reordering.

If we were using a compiler based on direct access objects (such as the HotSpot default compiler), this would not be a problem without the volatile keyword.

If we use a compiler (such as the Symantec JIT) that accesses objects in a handled manner, leaving the volatile keyword out may result in reordering and returning an uninitialized instance.

This conclusion is not guaranteed to be true, but is a guess based on the information currently available, and confirmation may require further experiments. If you have Yan Jin’s theory or more detailed experimental data, please feel free to contact me.