О чем эта статья?
Данная статья посвящена паттерну проектирования Singleton и о способах его реализации. Мы разберем ключевые моменты при реализации, а также посмотрим в чем может быть проблема, если данные моменты не учесть.
Фаза 1. Осознание
В первую очередь, говоря о паттерне Singleton, стоит разделять паттерн от виски Lazy Singleton и Eager Singleton. Как понятно из названия, один из них создается лишь при необходимости, единожды в программе, другой же существует со старта. Давайте разберем Lazy Singleton, как более сложный и менее очевидный при реализации.
Фаза 2. Первая попытка
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
return new Singleton();
}
return singleton;
}
}
Самый простой метод реализации Lazy Singleton, у которого одна большая проблема - многопоточность. Из-за race condition по итогу у нас получится не Singleton, а doubleton, trippleton и т д. Давайте попробуем решить эту проблему.
Фаза 3. Осознание
Самое очевидное, что приходит сразу на ум, это поставить synchronized:
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (singleton == null) {
return new Singleton();
}
return singleton;
}
}
Однако, это тоже плохое решение, так как если объект нужен большому количеству бизнес процессов в программе, то система постоянно будет виснуть на блокировках, поэтому просто synchronized нам не подойдет, нужно что-то лучше.
Фаза 4. Апгрейд
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
Теперь же все выглядит куда лучше, мы ввели double check, если объект есть, мы его можем сразу же получить, если же его нет, тогда мы ставим блокировку и создаем его, в случае если он еще не создан другим потоком. Однако, тут тоже есть проблема и так сразу ее не увидеть. Новая IntelliJ уже умеет ее определять и в данном случае уже пытается нас спасти, выводя предупреждение: Make 'singleton' volatile. А все дело в оптимизациях Java, а если конкретнее, оптимизации Out-Of-Order Execution, суть которой, если вкратце, не копировать каждый раз на стек значение переменной, если этого не требуется, тем самым уменьшая количество операций с памятью.
По итогу, благодаря этой оптимизации, все эти проверки не имеют смысла и мы откатились к фазе 2.
Фаза 5. Lazy Singleton
public 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, мы указываем, что переменная чувствительна и к ней не нужно применять оптимизации, благодаря чему мы смогли написать паттерн Singleton, который не создает баги.
Eager Singleton
Однако, помимо Lazy Singleton есть Eager Singleton, который куда проще и очевиднее, способов его реализации несколько.
Реализация JetBrains:
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
Реализация через enum:
public enum Singleton {
INSTANCE;
public void doWork() {
System.out.println("Singleton is whiskey");
}
}
Также есть реализации через внутренний, вложенный класс.
Таким образом, мы рассмотрели различные варианты синглтоноварения, однако, тут все еще есть проблемы, ведь как мы знаем, Singleton является как паттерном, так и антипаттерном и подобные Singleton'ы, написанные своими руками в программе, являются антипаттернами, а причину этого мы возможно рассмотрим в следующих статьях.