[Design Pattern](EN) Singleton Pattern Concept and Example

Post about Singleton Pattern.

This post is based on book ‘JAVA 객체지향 디자인 패턴’.


Environment and Prerequisite

  • Java


What is Singleton Pattern?

Singleton Pattern

  • Singleton Pattern : Software design pattern that restricts the instantiation of a class to one single instance. It access to its instance by using like getInstance() method or function.
  • Image Source : https://en.wikipedia.org/wiki/Singleton_pattern


Implementation

Usual Case Implementation

  • Let’s imagine we make printer.
  • Consider printer should be only one instance, so then we can use singleton pattern like below.

Printer

public class Printer {
    private static Printer printer = null;
    private Printer() {}

    public static Printer getPrinter(){
        if(printer == null){
            printer = new Printer();
        }
        return printer;
    }

    public void print(String str){
        System.out.println(str);
    }
}

User

public class User {
    private String name;

    public User(String name){
        this.name = name;
    }

    public void print() {
        Printer.getPrinter().print(this.name + " " + "print using" + " " + Printer.getPrinter().toString());
    }
}

Main

public class Main {
    public static void main(String[] args) {
        final int CNT = 6;
        User[] user = new User[CNT];
        for (int i = 0; i < CNT; i++){
            user[i] = new User(i + "-user");
            user[i].print();
        }
    }
}

Result

0-user print using com.company.Printer@60e53b93
1-user print using com.company.Printer@60e53b93
2-user print using com.company.Printer@60e53b93
3-user print using com.company.Printer@60e53b93
4-user print using com.company.Printer@60e53b93
5-user print using com.company.Printer@60e53b93
  • Above class makes instance if there is no instance. If instance exists, then return the instance.
  • Use static to make printer variable and getPrinter() method. printer variable and getPrinter() method are belong to class so it can be used without instance.
  • By using like this, we can make only one instance.
  • Result shows that there is only one printer instance.


Problem

  • There can be a race condition if multi threads access to this instance simultaneously before instantiation.
  • There can be multiple instances if threads access simultaneously before making instance.
  • Below example uses Thread.sleep(1) to make multiple instances.

Printer

public class Printer {
    private static Printer printer = null;
    private Printer() {}

    public static Printer getPrinter(){
        if(printer == null){
            try{
                Thread.sleep(1);
            }
            catch (InterruptedException e) { }
            printer = new Printer();
        }
        return printer;
    }

    public void print(String str){
        System.out.println(str);
    }
}

UserThread

public class UserThread extends Thread {
    public UserThread(String name){
        super(name);
    }

    public void run(){
        Printer.getPrinter().print(Thread.currentThread().getName()
                + " " + "print using" +
                " " + Printer.getPrinter().toString());
    }
}

Main

public class Main {
    public static void main(String[] args) {
        final int CNT = 6;
        UserThread[] user = new UserThread[CNT];
        for (int i = 0; i < CNT; i++){
            user[i] = new UserThread(i + "-user");
            user[i].start();
        }
    }
}

Result

0-user print using com.company.Printer@4f90c612
5-user print using com.company.Printer@17a724
4-user print using com.company.Printer@d156cd4
3-user print using com.company.Printer@4f90c612
1-user print using com.company.Printer@6fa76e7
2-user print using com.company.Printer@4f90c612
  • Like this, if threads access simultaneously then it fails to make one instance in singleton pattern.


Solution - Instantiate when initialize class

  • Make instance immediately when make variable in class.
  • Like private static Printer printer = new Printer();, make instance immediately.

Printer

public class Printer {
    private static Printer printer = new Printer();
    private Printer() {}

    public static Printer getPrinter(){
        return printer;
    }

    public void print(String str){
        System.out.println(str);
    }
}

UserThread

public class UserThread extends Thread {
    public UserThread(String name){
        super(name);
    }

    public void run(){
        Printer.getPrinter().print(Thread.currentThread().getName()
                + " " + "print using" +
                " " + Printer.getPrinter().toString());
    }
}

Main

public class Main {
    public static void main(String[] args) {
        final int CNT = 6;
        UserThread[] user = new UserThread[CNT];
        for (int i = 0; i < CNT; i++){
            user[i] = new UserThread(i + "-user");
            user[i].start();
        }
    }
}

Result

3-user print using com.company.Printer@651f2f3d
1-user print using com.company.Printer@651f2f3d
2-user print using com.company.Printer@651f2f3d
0-user print using com.company.Printer@651f2f3d
4-user print using com.company.Printer@651f2f3d
5-user print using com.company.Printer@651f2f3d
  • All threads access to only one instance.


Solution - Synchronize instance construction method

  • Use synchronized to prevent from accessing multiple threads to method simultaneously.

Printer

public class Printer {
    private static Printer printer = null;
    private Printer() {}

    public synchronized static Printer getPrinter(){
        if(printer == null){
            printer = new Printer();
        }
        return printer;
    }

    public void print(String str){
        System.out.println(str);
    }
}

UserThread

public class UserThread extends Thread {
    public UserThread(String name){
        super(name);
    }

    public void run(){
        Printer.getPrinter().print(Thread.currentThread().getName()
                + " " + "print using" +
                " " + Printer.getPrinter().toString());
    }
}

Main

public class Main {
    public static void main(String[] args) {
        final int CNT = 6;
        UserThread[] user = new UserThread[CNT];
        for (int i = 0; i < CNT; i++){
            user[i] = new UserThread(i + "-user");
            user[i].start();
        }
    }
}

Result

3-user print using com.company.Printer@8f62e66
4-user print using com.company.Printer@8f62e66
0-user print using com.company.Printer@8f62e66
1-user print using com.company.Printer@8f62e66
2-user print using com.company.Printer@8f62e66
5-user print using com.company.Printer@8f62e66
  • All threads access to only one instance.


Problem

  • Even though above multiple thread problems are solved, if there is variable in class like below then there comes another problem.
  • cnt’s value is not consistent.

Printer

public class Printer {
    private static Printer printer = null;
    private int cnt = 0;
    private Printer() {}

    public synchronized static Printer getPrinter(){
        if(printer == null){
            printer = new Printer();
        }
        return printer;
    }

    public void print(String str){
        cnt++;
        System.out.println(str + " " + "cnt : " + cnt);
    }
}

UserThread

public class UserThread extends Thread {
    public UserThread(String name){
        super(name);
    }

    public void run(){
        Printer.getPrinter().print(Thread.currentThread().getName()
                + " " + "print using" +
                " " + Printer.getPrinter().toString());
    }
}

Main

public class Main {
    public static void main(String[] args) {
        final int CNT = 6;
        UserThread[] user = new UserThread[CNT];
        for (int i = 0; i < CNT; i++){
            user[i] = new UserThread(i + "-user");
            user[i].start();
        }
    }
}

Result

2-user print using com.company.Printer@6fa76e7 cnt : 2
1-user print using com.company.Printer@6fa76e7 cnt : 4
3-user print using com.company.Printer@6fa76e7 cnt : 3
0-user print using com.company.Printer@6fa76e7 cnt : 2
4-user print using com.company.Printer@6fa76e7 cnt : 5
5-user print using com.company.Printer@6fa76e7 cnt : 6


Solution - Add synchronized in method

  • Cover method with synchronized() to prevent from simultaneous threads access.

Printer

public class Printer {
    private static Printer printer = null;
    private int cnt = 0;
    private Printer() {}

    public synchronized static Printer getPrinter(){
        if(printer == null){
            printer = new Printer();
        }
        return printer;
    }

    public void print(String str){
        synchronized (this){
            cnt++;
            System.out.println(str + " " + "cnt : " + cnt);
        }
    }
}

UserThread

public class UserThread extends Thread {
    public UserThread(String name){
        super(name);
    }

    public void run(){
        Printer.getPrinter().print(Thread.currentThread().getName()
                + " " + "print using" +
                " " + Printer.getPrinter().toString());
    }
}

Main

public class Main {
    public static void main(String[] args) {
        final int CNT = 6;
        UserThread[] user = new UserThread[CNT];
        for (int i = 0; i < CNT; i++){
            user[i] = new UserThread(i + "-user");
            user[i].start();
        }
    }
}

Result

0-user print using com.company.Printer@4f90c612 cnt : 1
5-user print using com.company.Printer@4f90c612 cnt : 2
4-user print using com.company.Printer@4f90c612 cnt : 3
2-user print using com.company.Printer@4f90c612 cnt : 4
1-user print using com.company.Printer@4f90c612 cnt : 5
3-user print using com.company.Printer@4f90c612 cnt : 6
  • Now cnt is printed well and there is only one instance.


Comparison between singleton pattern and static class


Reference