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, чтобы обеспечить корректную публикацию объекта между потоками.