How to make Singleton design pattern in Java?

By | July 8, 2023

The singleton design pattern restricts a class to having only one single instance and provides a global point of access to that single instance. In other words, A singleton class must return the same instance no matter how many times an application requests it. i.e., the same single instance should be used by all the classes of the application.

Singleton example in Java

If we have an application to perform the CRUD Operation, we need to connect our application to the database. Say we are using JDBC to connect our application to the persistence layer to perform CRUD operations. In this case, we need to have only one connection between our application and database to perform all the operations; it must not be like for every different operation we create a new connection, which will reduce the performance of the application. For example, for CREATE, we have one connection, for READ, we have one connection, and so on.

Singleton design pattern
Fig 1- Singleton object: Jdbc connection

Advantages of Singleton design pattern

  • The singleton design pattern ensures that the class has only one instance.
  • It is memory efficient as a single instance of the class is reused, and no new instance is created again and again.

Disadvantages of Singleton design pattern

  • The singleton design pattern leads to dependency hiding as we use global instances of the singleton class, which restrict other classes of the application from seeing what dependencies are getting used in the singleton class.
  • The single instance of the class is carried throughout the lifetime of the application, which sometimes affects unit testing.
  • A singleton class may lead to a tightly coupled code base.

How to create Singleton Class in Java

We will learn how to achieve a singleton pattern in real time. For this, we are going to create a Java application and add two classes to it. i.e., Book and Main class. We will make our book class as singleton class in java.

Step 1- Create a Java project

First, we need to create a Java project and we add the following class two classes to our project.

Fig 2- Java project

We add a constructor to our Book class. Below is a snippet of our Book class.

Book.java
package com.paulsofts.data;

public class Book {
	
	//constructor
	public Book() {
		System.out.println("calling Book class constructor...");
	}
	
}

We create an object of the Book class in our Main class by calling the constructor of the Book class. Below is a snippet of our Main class.

Main.java
package com.paulsofts.main;

import com.paulsofts.data.Book;

public class Main {
	
	public static void main(String[] args) {

		//creating the object of Book class
		new Book();
	}

}

As we will create the object of the Book class in our Main class, it is going to call the constructor of the Book class.

Fig 3- Object creation: constructor call

Above, we can see that as we have run our Main class, it is calling the constructor of Book class; similarly, if other classes create the object of Book class, they will be calling the constructor of Book class. So, we need to somehow hide the constructor of the Book class so that no other classes will be able to call its constructor.

Step 2- Making the constructor private

In order to hide the constructor of the Book class from other classes, we need to make its constructor private. This will ensure that the constructor is available only to the Book class and not the other classes.

Fig 4- Making the constructor private
Book.java
package com.paulsofts.data;

public class Book {
	
	//making the constructor private
	private Book() {
		System.out.println("calling Book class constructor...");
	}
	
}

As soon as we make the constructor private, it causes an error in our Main class, where we were trying to create an object by calling the constructor. As the private keyword makes the constructor available where it is defined. i.e., only in Book class and hide it from other classes.

Fig 5- Book() constructor: Not visible
Main.java
package com.paulsofts.main;

import com.paulsofts.data.Book;

public class Main {
	
	public static void main(String[] args) {

		//creating the object of Book class
		new Book();
	}
}

Step 3- Factory method to return instance

Now, we have restricted the other classes from creating the object of our Book class by making its constructor private. But we need to find a way to create a single instance that will be returned as many times as the other classes request it. For this, we create a factory method that will return the same single instance of an object of the Book class whenever any other class requests it.

Book.java
package com.paulsofts.data;

/**
 * 1. private constructor: 
 * 		- Hides it from other classes
 * 
 * 2. getBook(): 
 * 		- Factory method
 * 		- Returns the object of book class
 * 
 * 3. Book class object:
 * 		- We are able to create the object of private
 * 		  constructor here, as private member are accessible
 * 		  in the class they are defined
 * 		
 * @author paulsofts 
 */

public class Book {
	
	private Book() {
		
	}
	
	public static Book getBook() {
		
		Book book = new Book();
		
		return book;
	}
}

Above, we can see that the getBook() method is returning the object of the Book class. Now, we will be calling this method in our Main class. We are going to create two different objects and call the hashCode() method on those objects. Note: A hash code is a unique number assigned to every object.

Java
package com.paulsofts.main;

import com.paulsofts.data.Book;

/**
 * 1. we will call the static method getBook()
 * using class-name
 * 
 * 2. HashCode is a unique number assigned to every object
 * we are calling hashCode() method on objects to check
 * the uniqueness of object
 * 
 * @author paulsofts
 * 
 */

public class Main {
	
	public static void main(String[] args) {

		Book book_obj_1 = Book.getBook();
		System.out.println("Hashcode for 1st obj: " + book_obj_1.hashCode());
		
		Book book_obj_2 = Book.getBook();
		System.out.println("Hashcode for 2nd obj: " + book_obj_2.hashCode());
		
	}

}

On running the Main class, we are getting following output:

Fig 6- Different hash code for different objects

As we see above, we are getting different hashCode values, which means that although we hide the constructor, two different objects are still getting created, so Singleton is still not achieved.

Step 4- Global instance variable declaration

In the above step, we see that singletons are not achieved as multiple instances are being created. This is because every time we call the getBook() method, it creates a new object of the Book class and returns it. We can overcome this issue by adding a check condition that, if an object is not created, creates an object using a constructor call, and if an object is already created, returns that object itself.

Book.java
package com.paulsofts.data;

/**
 * 1. create an instance of Book class
 * 		- book: it's default value is null
 * 
 * 2. private constructor: 
 * 		- Hides it from other classes
 * 
 * 3. getBook(): 
 * 		- Factory method
 * 		- Checks if instance variable- "book" is null, 
 * 		  then, create an object of Book class
 * 		- Returns that object of Book class
 * 
 * 4. Book class object:
 * 		- We are able to create the object of private
 * 		  constructor here, as private member are accessible
 * 		  in the class they are defined
 * 		
 * @author paulsofts 
 */

public class Book {
	
	private static Book book;
	
	private Book() {
		
	}
	
	public static Book getBook() {
		
		if(book == null) {
			book = new Book();
		}
		
		return book;
	}
}

As we can see, we have created a static reference variable called book because we need to use it inside the static getBook() method. If this is the first time any of the classes are trying to create an object of the Book class, it will check if the book contains a null value and create the object of the Book class; otherwise, it will return the previously created object.

Main.java
package com.paulsofts.main;

import com.paulsofts.data.Book;

/**
 * 1. we will call the static method getBook()
 * using class-name
 * 
 * 2. HashCode is a unique number assigned to every object
 * we are calling hashCode() method on objects to check
 * the uniqueness of object
 * 
 * @author paulsofts
 * 
 */

public class Main {
	
	public static void main(String[] args) {

		Book book_obj_1 = Book.getBook();
		System.out.println("Hashcode for 1st obj: " + book_obj_1.hashCode());
		
		Book book_obj_2 = Book.getBook();
		System.out.println("Hashcode for 2nd obj: " + book_obj_2.hashCode());
		
	}

}

Now, if we run our main method, it will return the same hash code value for both the objects, which means a single object is getting created, and hence Singleton is achieved.

Fig 7- Single object: same hashcode

How to implement Singleton design pattern

We can implement singleton design pattern in two ways:

  1. Early initialization
    • Instance is created at the time of class loading.
  2. Lazy initialization
    • Instance is created whenever required.

1. Singleton design pattern- Early initialization

Early initialization is also known as early loading. In this, we create the single sole instance of the class at the time of the declaration of the static data member of the class. i.e., the instance variable of the class. This causes an instance of the class to be created at the time of class loading.

Book.java
package com.paulsofts.data;

/**
 * 1. Declare a static reference variable of Book class
 *    initialize it with Book class object
 * 
 * 2. Book class object will gets created as soon as class
 *    is loaded in the memory
 * 
 * 3. Define getBook() method which is the global point of
 *    access to singleton object of Book class 
 * 		
 * @author paulsofts 
 */

public class Book {
	
	private static Book book = new Book();
	
	private Book() {
		
	}
	
	public static Book getBook() {
		
		return book;
	}
	
}

Now, we call the getBook() method of the Book class in our Main class and check if singleton is achieved or not.

Main.java
package com.paulsofts.main;

import com.paulsofts.data.Book;

/**
 * 1. we will call the static method getBook()
 * using class-name
 * 
 * 2. HashCode is a unique number assigned to every object
 * we are calling hashCode() method on objects to check
 * the uniqueness of object
 * 
 * @author paulsofts
 * 
 */

public class Main {
	
	public static void main(String[] args) {

		Book book_obj_1 = Book.getBook();
		System.out.println("Hashcode for 1st obj: " + book_obj_1.hashCode());
		
		Book book_obj_2 = Book.getBook();
		System.out.println("Hashcode for 2nd obj: " + book_obj_2.hashCode());
		
	}

}

Once we run our Main class, we get the following output:

Fig 8- Early loading output

As we can see, we are getting the same hash code for both the objects, and hence only one single object is getting created, and it gets created at the time of class loading.

  • Pros
    • Early initialization is thread-safe.
    • It is very easy to implement.
  • Cons
    • In early initialization, the instance is created at the class loading time, which means the instance of the class is available all the time even if it is not required.
    • This may lead to a waste of resources as objects are created in the beginning even if they are not required.

2. Singleton design pattern- Lazy initialization

In lazy initialization, an instance of a class is created using a synchronized method or synchronized block, and the instance of the class is created whenever it is required. Lazy initialization also prevents resource waste. Below, we can see the implementation of lazy initialization for single- and multi-threaded environments.

2.1 Lazy initialization implementation for Single thread

Below, we have an implementation for lazy initialization for a single thread. In this implementation, we have created a reference variable for the Book class whose default value is null, and in the getBook() method, we are checking if it is null. If true, we will initialize it with the Book class object.

Book.java
package com.paulsofts.data;

/**
 * 1. Declare a static reference variable of Book class
 *    the default value of book is null
 * 
 * 2. Make the constructor private, this will hide
 *    Book's constructor from other classes
 * 
 * 3. In getBook() method which is the global point of
 *    access to singleton object we are checking if
 *    reference variable contains null then initialize
 *    otherwise returns the previously created object
 * 		
 * @author paulsofts 
 */

public class Book {
	
	private static Book book;
	
	private Book() {
		
	}
	
	public static Book getBook() {
		if(book == null) {
			book = new Book();
		}
		return book;
	}
}

This implementation works well with single-threaded applications, but in multi-threaded applications, it will lose the singleton behaviour and create multiple objects. Say we have two threads, t1 and t2. As we know, threads run concurrently, so if t1 and t2 both try to call the getBook() method simultaneously, a race condition will occur and two instances of the Book class will be created.

Main.java
package com.paulsofts.main;

import com.paulsofts.data.Book;

/**
 * 1. we will call the static method getBook()
 *    using class-name
 * 
 * 2. HashCode is a unique number assigned to every object
 *    we are calling hashCode() method on objects to check
 *    the uniqueness of object
 * 
 * 3. We have created two threads t1 and t2, and we are
 *    trying to call getBook() method using t1 and t2 in
 *    multi-threaded environment
 * 
 * @author paulsofts
 * 
 */

public class Main extends Thread {
	
	
	public void run() {
		
		Book book_obj_1 = Book.getBook();
		System.out.println("Hashcode for 1st obj: " + book_obj_1.hashCode());
		
	}
	
	public static void main(String[] args) {

		Main t1 = new Main();
		Main t2 = new Main();
		
		t1.start();
		t2.start();
	}
}

It may be possible; we may get the same hash code for both threads, but if we run the application multiple times, we can see two different instances are getting created for the Book class, and hence the above implementation is not thread-safe.

Fig 9- Lazy initialization output

2.2 Lazy initialization implementation for Multi thread

A: Synchronized Singleton

In this implementation of lazy initialization, we synchronized the method that returns the singleton instance of the class. This way, we will prevent the race condition from occurring and achieve the singleton pattern for a multi-threaded environment.

As we can see, we have synchronized the getBook() method with the help of the synchronized keyword. This will prevent multiple threads from entering the getBook() method simultaneously, and hence only one single instance will be created.

Book.java
package com.paulsofts.data;

/**
 * 1. Declare a static reference variable of Book class
 *    the default value of book is null
 * 
 * 2. Make the constructor private, this will hide
 *    Book's constructor from other classes
 *    
 * 3. We synchronized the getBook() method that returns the 
 *    instance of the Book class. Synchronized keyword
 *    prevent the race condition and only one thread
 *    can enter inside the getBook() method at a time
 * 
 * 4. In getBook() method which is the global point of
 *    access to singleton object we are checking if
 *    reference variable contains null then initialize
 *    otherwise returns the previously created object
 * 		
 * @author paulsofts 
 */

public class Book {
	
	private static Book book;
	
	private Book() {
		
	}
	
	public synchronized static Book getBook() {
		if(book == null) {
			book = new Book();
		}
		return book;
	}
}

Now, if we run our Main class, we will get the singleton instance of our Book class. No matter how many times we run our application.

Java
package com.paulsofts.main;

import com.paulsofts.data.Book;

/**
 * 1. we will call the static method getBook()
 *    using class-name
 * 
 * 2. HashCode is a unique number assigned to every object
 *    we are calling hashCode() method on objects to check
 *    the uniqueness of object
 * 
 * 3. We have created two threads t1 and t2, and we are
 *    trying to call getBook() method using t1 and t2 in
 *    multi-threaded environment
 * 
 * @author paulsofts
 * 
 */

public class Main extends Thread {
	
	
	public void run() {
		
		Book book_obj_1 = Book.getBook();
		System.out.println("Hashcode for 1st obj: " + book_obj_1.hashCode());
		
	}
	
	public static void main(String[] args) {

		Main t1 = new Main();
		Main t2 = new Main();
		
		t1.start();
		t2.start();
		
	}
}

We can see, we are getting the same hash code for both the threads t1 and t2.

Fig 10- Synchronized singleton output

Let’s understand the problem in the above implementation. Say we have two threads, t1 and t2, and both try to enter the getBook() method, but because of synchronization, only one can get a chance. Assume that t1 gets a chance to enter the getBook() method, and t2 has to wait until t1 finishes its execution. Once t1 finished its execution, t2 entered the block and saw that the book instance variable was not null, so it moved out and got the object created previously by t1. So, every time multiple threads try to enter the synchronized method, others have to wait until the first thread that enters the block completes its execution.

B: Synchronized Singleton- Double check lock singleton

In this implementation, we add the double-check lock system to our singleton class. This will take care of the issue of the above implementation of the singleton class. i.e., no thread has to wait to enter the synchronized method or block; this will improve the time complexity of the application.

Book.java
package com.paulsofts.data;

/**
 * 1. Declare a static reference variable of Book class
 *    the default value of book is null
 * 
 * 2. Make the constructor private, this will hide
 *    Book's constructor from other classes
 *    
 * 3. In getBook() method, first null check is first lock
 *    after that inside synchronized block, second null
 *    check represents the second lock
 * 
 * 4. In getBook() method which is the global point of
 *    access to singleton object we are checking if
 *    reference variable contains null then initialize
 *    otherwise returns the previously created object
 * 		
 * @author paulsofts 
 */

public class Book {
	
	private static Book book;
	
	private Book() {
		
	}
	
	public static Book getBook() {
		//first if-block
		if(book == null) {
			synchronized(Book.class) {
				//second if-block
				if(book == null) {	
					book = new Book();
				}
			}
		}
		return book;
	}
}

In the above implementation, If two threads, t1 and t2, enter the getBook() method, they check that the reference variable (book) is null and move inside the first if-block. After that, due to the synchronized key, any one of them will get a chance to enter the synchronized block. If t1 enters, it will initialize the book, create an instance of the Book class, and come out. After ward t2 enters and sees the second if-block, the book is not null, so it will also return out. Now, from the second time onward, no threads have to wait, or they are not able to reach even the synchronized block because of the first if-block, as the book reference is not null. Let’s say t3 and t4 enter the getBook() method, and at the first if-block they will be stopped and move out.

Leave a Reply

Your email address will not be published. Required fields are marked *