43

Singleton Pattern: Why Your Singleton Might not Be a Singleton

 5 years ago
source link: https://www.tuicool.com/articles/hit/3IvAJba
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
EFZraai.jpg!web

The singleton pattern is definitely the easiest pattern of all the design patterns, but trust me — many of us are doing wrong. So, let’s find out more about this pattern.

We all know the motivation of using the Singleton pattern. It’s a pattern that allows us to have only a single instance of the object throughout the life-cycle of the application. Why would that be important? Because first, you don’t want to create more than one heavy resource consuming objects when you really want only one. Second, sometimes, not only creating more than one instance is costly, it can take you to an inconsistent state . For example, you wouldn’t want to have multiple database objects because changes in one may make others inconsistent.

But, you might be thinking, if I just need one object and I want to access it from anywhere, why can’t I make it a global variable? The answer is, yes, you can make it a global variable. But, what if that object creation requires heavy resource usage? Global variables might be created just when your application starts. You wouldn’t want too much of startup time for your Android app, right? These small things are easily noticeable in the mobile world. To prevent this, we make use of lazy-loading, which means that we need to create the object only when it is needed. We’ll see that in a second.

Now that we are clear why we want a singleton object, let’s see the characteristics of a singleton object.

A singleton class shouldn’t allow other classes to instantiate it. That means, no class should be able to do new Singleton() at all. How do we prevent this? This one is simple. Just make the constructor of the singleton class private . This simple step makes sure that the object won’t be instantiated anywhere — except within the same class. Private constructors can be called from the same class. This is true for private methods and fields, too. Below is a simple example in Kotlin of a class with private constructors.

class Singleton private constructor()

A singleton class should be accessible to the class. Even though we have restricted instantiation of the singleton class, we can still have access to that kind of object. This is by making a static method, which returns the singleton object. In the following example, we have made a static method that checks to see if the instance is null. If so, it will instantiate a new object and assign it to the INSTANCE variable, which will be returned on subsequent calls to the getInstance method.

class Singleton private constructor(){
  companion object {       
    private var INSTANCE: Singleton ? = null
      fun getInstance(): Singleton{ 
      if(INSTANCE == null){   
        INSTANCE = Singleton()    
      }           
      return INSTANCE!!     
    }    
  }
}

Most of the people who implement the singleton pattern get the above two concepts right. But, the problem with the above code is that it may produce more than one instance of your singleton class in a multi-threaded environment.

fun getInstance(): Singleton{     
  if(INSTANCE == null){       
    // <--- Imagine Thread 2 is here
INSTANCE = Singleton() // <--- Imagine Thread 1 is here
}...

In the above code, both Thread 1 and Thread 2 will produce two distinct objects, defeating the purpose of a singleton class. This can be catastrophic to your application and might be difficult to debug, because as with other multi-threaded issues, they only happen in some circumstances.

So, an easy and no-brainer fix would be to just execute the code within a synchronized lock. That would go like this:

fun  getInstance(): Singleton {  
  synchronized(this) {    
    if(INSTANCE == null){   
      INSTANCE = Singleton()    
    }       
    return INSTANCE!!
  }          
}

The above code will solve the case of the creation of multiple instances, but it may pose a performance problem. This means, even after correctly instantiating a singleton object, the subsequent access to it will be synchronized, which is not necessary. There is no issue in reading an object in a multi-threaded environment. A good ballpark metric would be to consider that any block under synchronized code-block will slow down the execution of that block by a factor of 100 . If you are OK with this cost and you don’t need to access that singleton object too often, you can stop here. But, if you do need to access it multiple times, it would help if you optimize it.

To optimize it, let’s think what we actually need to optimize. We need to optimize only the object creation flow — not the reading of the object flow.

After the INSTANCE variable is created, synchronizing the flow is totally an unneeded overhead.

An easy fix here would be to create the object eagerly rather than lazily. Here’s what it would look like:

class Singleton private constructor(){  
  companion object {       
    val INSTANCE = Singleton()  
      fun  getInstance(): Singleton {
      return INSTANCE             
    }  
  }
}

In the above case, JVM will make sure that any thread accesses the INSTANCE variable after it has been created. But, then again, as we discussed, it makes it add up to your startup time where you are creating an object, even though you’d need to later or won’t need it ever.

So, we will now take a look at “double-checked locking,” which can help us overcome all the above issues that we’ve been talking about. In double-check locking, we’ll first see if the object is created or not. If not, then we will apply for synchronization.

companion object { 
  @Volatile private var INSTANCE: Singleton ? = null 
    fun  getInstance(): Singleton {  
    if(INSTANCE == null){            
      synchronized(this) {         
        INSTANCE = Singleton()     
      }          
    }         
    return INSTANCE!! 
  }
}

As you can see, we are only applying synchronization during the object instantiation phase.

You might be thinking, what is that @Volatile annotation doing there. It’s the same as the  volatile keyword in Java. As you know, each thread has its copy of variables in its stack, and all the accessible values are copied to its stack when the thread is created. Adding a volatile keyword is like an announcement that says, “The value of this variable might change in some other thread.” Adding volatile ensures that the value of the variable is refreshed. Otherwise, it may so happen that the thread never updates its local cache.

As a final refactor, I’d like to make a small change and leverage Kotlin’s language construct. I really don’t like the screaming !! in the return statement.

companion object {   
  @Volatile private var INSTANCE: Singleton ? = null     
    fun  getInstance(): Singleton {      
    return INSTANCE?: synchronized(this){ 
      Singleton().also {            
        INSTANCE = it           
      }        
    }   
  }
}

So, there you have it— the perfect way to make a singleton. Your implementation may differ based on other considerations, like your application not running on a multi-threaded environment or you are fine with the performance cost of putting synchronize keyword over the entire method.

Thanks for reading!

Here are a few other articles that might interest you:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK