Реализовать потокобезопасный Singleton

79. Реализовать потокобезопасный Singleton

Условие задачи:

Реализовать паттерн Singleton — класс, для которого во время работы приложения может существовать только один экземпляр.

Метод getInstance() должен при каждом вызове возвращать один и тот же объект.

Необходимо также учесть возможность обращения к Singleton из нескольких потоков.

Код:

public class Main {

    public static void main(String[] args) {
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
    }
}

Все вызовы Singleton.getInstance() должны возвращать один и тот же экземпляр класса.


Спойлеры к решению

Подсказки

💡 Конструктор Singleton должен быть private, чтобы экземпляр класса нельзя было создать снаружи через new.

💡 Единственный экземпляр можно хранить в статическом поле класса.

💡 Создать объект можно лениво — только при первом вызове getInstance().

💡 Если getInstance() может одновременно вызываться из нескольких потоков, необходимо защитить момент создания объекта от race condition.


Решение

Один из простых способов реализовать потокобезопасный Singleton — синхронизировать участок кода, отвечающий за создание экземпляра.

public class Main {

    public static void main(String[] args) {
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
    }
}

final class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

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

        return singleton;
    }
}

Конструктор объявлен как private, поэтому создать объект следующим образом невозможно:

Singleton singleton = new Singleton();

Доступ к экземпляру осуществляется только через статический метод:

Singleton.getInstance();

При первом вызове поле singleton содержит null, поэтому создаётся новый объект:

if (singleton == null) {
    singleton = new Singleton();
}

При последующих вызовах новый объект уже не создаётся, и метод возвращает существующий экземпляр.

Блок:

synchronized (Singleton.class)

нужен для потокобезопасности. Без синхронизации два потока теоретически могут одновременно увидеть:

singleton == null

и создать два разных экземпляра.

При запуске программы будет выведена ссылка на один и тот же объект:

Singleton@6d311334
Singleton@6d311334
Singleton@6d311334
Singleton@6d311334
...

То есть каждый вызов getInstance() возвращает один экземпляр.

У такой реализации есть недостаток: синхронизация выполняется при каждом вызове getInstance(), хотя после создания объекта она уже не требуется.

Более эффективный вариант можно реализовать через double-checked locking:

final class Singleton {

    private static volatile Singleton singleton;

    private Singleton() {
    }

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

        return singleton;
    }
}

Здесь синхронизация выполняется только во время первоначального создания объекта.

Поле обязательно объявляется как volatile, чтобы обеспечить корректную публикацию объекта между потоками.