Много статей написано о том, как правильно реализовывать на Java шаблон проектирования Singleton.
Как правило, специалисты ломают копья вокруг проблемы, как совместить корректную работу в условиях многопоточного использования и эффективное выполнение, обеспечивающее производительность, близкую максимальной.
Лично я считаю единственным корректным способом реализации синглтона на Java так называемый Synchronized Accessor:
Именно так задумывали реализацию подобной задачи авторы виртуальной машины Java, именно такая реализация используется в стандартной библиотеке классов языка Java. Если же для программы метод доступа к синглтону становится узким местом, то это повод для того, чтобы произвести редизайн программы, чтобы она обращалась к глобальному объекту не так часто.
Однако, пытаясь освежить в памяти возможности Java concurrency, я почитал старые статьи о вариантах синглтонов и удивился, что не нахожу описания еще одного способа, который я называю Function Pointer.
Function Pointer заслуживает внимание тем, что он годится не только для шаблона Singleton, но и для многих других задач. Его можно использовать тогда, когда в программе существует несколько ветвей кода, одна из которых, начиная с какого-то момента, становится единственной. Остальные ветви остаются зомби-кодом, который уже никогда не будет выполняться. Как раз такая ветка присутствует в большинстве вариантов синглтона.
Идея заключается в том, что мы создаем интерфейс с одним методом и несколько реализаций. Каждая ветка помещается в отдельную имплементацию интерфейса. В конце метода добавляем установку в глобальной переменной (Function Pointer) той реализации, которая должна будет отработать в следующий раз.
Вот как это выглядит:
При первом вызове Singleton.getInstance() управление получит объект SynchronizedFunction. Его метод get() объявлен как synchronized, поэтому для потоков, которые его вызывают, будут справедливы отношения happens-before. Это гарантирует нам, что код
Все потоки, которые вызовут Singleton.getInstance() после окончания SynchronizedFunction.get(), перейдут по сслыке в метод DummyFunction.get(), который уже не содержит ни условий, ни критических секций. Это просто метод-аксессор, который возвращает всегда одно и то же значение.
Нам даже не нужно объявлять поля volatile. Если какой-то поток не увидит записи в поле accessor, то он попадет в synchronized-метод, где он обязан будет увидеть все обновления, сделанные другими потоками в этом методе.
Ну а в классе DummyFunction единственное поле объявлено final. Java нам гарантирует, что final-поля видны одинаково во всех потоках.
При компиляции данного кода Eclipse сообщает, что доступ к полям с видимостью private компилятор осуществляет каким-то нестандартным менее эффективным способом и советует из соображений производительности увеличить видимость поля accessor и конструкторов. В данном случае все эти медленные обращения производятся не более 1 раза, поэтому советами компилятора можно пренебречь.
Часто в реализациях шаблона Singleton обращают внимание на возможность обработки ошибок, возникающих в конструкторе целевого класса. Сделаем конструктор, который срабатывает только со 2-го раза:
Легко убедиться, что наша реализация позволяет корректно поймать и обработать исключение, выброшенное из конструктора. В случае ошибки статическое поле accessor не будет изменено, и наш Singleton останется в исходном непроинициализированном состоянии.
Посмотрим, насколько эффективно сможет откомпилировать наш класс Hotspot JVM. Запустим Java с опциями -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining. Получаем вот такую диагностику:
Я не очень разбираюсь в сообщениях JIT-компилятора, но по-моему, это самое быстрое, что можно получить из метода getInstance(). Вместо него подставлен метод accessor, возвращающий значение одного из полей.
+ Гарантирован 1 экземпляр класса Singleton
+ Безопасность при многопоточном использовании
+ Ленивая инициализация
+ Обработка ошибок конструктора
+ Эффективная реализация
- Вызов виртуального метода при получении экземпляра Singleton
- Громоздкий код
- 3 дополнительных класса: интерфейс и 2 реализации
[1] Реализация Singleton в JAVA
[2] Правильный Singleton в Java
[3] Singleton pattern
[4] What is an efficient way to implement a singleton pattern in Java?
[5] The «Double-Checked Locking is Broken» Declaration
[6] Singleton Design Pattern
Как правило, специалисты ломают копья вокруг проблемы, как совместить корректную работу в условиях многопоточного использования и эффективное выполнение, обеспечивающее производительность, близкую максимальной.
Лично я считаю единственным корректным способом реализации синглтона на Java так называемый Synchronized Accessor:
public class Singleton {
private static Singleton instance;
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Именно так задумывали реализацию подобной задачи авторы виртуальной машины Java, именно такая реализация используется в стандартной библиотеке классов языка Java. Если же для программы метод доступа к синглтону становится узким местом, то это повод для того, чтобы произвести редизайн программы, чтобы она обращалась к глобальному объекту не так часто.
Однако, пытаясь освежить в памяти возможности Java concurrency, я почитал старые статьи о вариантах синглтонов и удивился, что не нахожу описания еще одного способа, который я называю Function Pointer.
Function Pointer заслуживает внимание тем, что он годится не только для шаблона Singleton, но и для многих других задач. Его можно использовать тогда, когда в программе существует несколько ветвей кода, одна из которых, начиная с какого-то момента, становится единственной. Остальные ветви остаются зомби-кодом, который уже никогда не будет выполняться. Как раз такая ветка присутствует в большинстве вариантов синглтона.
Идея заключается в том, что мы создаем интерфейс с одним методом и несколько реализаций. Каждая ветка помещается в отдельную имплементацию интерфейса. В конце метода добавляем установку в глобальной переменной (Function Pointer) той реализации, которая должна будет отработать в следующий раз.
Вот как это выглядит:
public final class Singleton {
private static FunctionPointer accessor = new SynchronizedFunction();
private Singleton() {
System.err.printf("%s.%s()%n", Singleton.class.getName(), Singleton.class.getSimpleName());
}
private static interface FunctionPointer {
Singleton get();
}
private static final class SynchronizedFunction implements FunctionPointer {
@Override
public synchronized Singleton get() {
if (accessor != this) {
return accessor.get();
}
Singleton instance = new Singleton();
accessor = new DummyFunction(instance);
return instance;
}
}
private static final class DummyFunction implements FunctionPointer {
private final Singleton instance;
private DummyFunction(Singleton instance) {
this.instance = instance;
}
@Override
public Singleton get() {
return instance;
}
}
public static Singleton getInstance() {
return accessor.get();
}
}
При первом вызове Singleton.getInstance() управление получит объект SynchronizedFunction. Его метод get() объявлен как synchronized, поэтому для потоков, которые его вызывают, будут справедливы отношения happens-before. Это гарантирует нам, что код
Singleton instance = new Singleton()
, выполнится ровно 1 раз. Другие потоки, вошедшие в SynchronizedFunction, увидят, что поле accessor уже изменено, и пройдут по условию accessor != this
.Все потоки, которые вызовут Singleton.getInstance() после окончания SynchronizedFunction.get(), перейдут по сслыке в метод DummyFunction.get(), который уже не содержит ни условий, ни критических секций. Это просто метод-аксессор, который возвращает всегда одно и то же значение.
Нам даже не нужно объявлять поля volatile. Если какой-то поток не увидит записи в поле accessor, то он попадет в synchronized-метод, где он обязан будет увидеть все обновления, сделанные другими потоками в этом методе.
Ну а в классе DummyFunction единственное поле объявлено final. Java нам гарантирует, что final-поля видны одинаково во всех потоках.
При компиляции данного кода Eclipse сообщает, что доступ к полям с видимостью private компилятор осуществляет каким-то нестандартным менее эффективным способом и советует из соображений производительности увеличить видимость поля accessor и конструкторов. В данном случае все эти медленные обращения производятся не более 1 раза, поэтому советами компилятора можно пренебречь.
Часто в реализациях шаблона Singleton обращают внимание на возможность обработки ошибок, возникающих в конструкторе целевого класса. Сделаем конструктор, который срабатывает только со 2-го раза:
private static final AtomicInteger cnt = new AtomicInteger();
private Singleton() {
if (cnt.incrementAndGet() == 1) {
throw new IllegalArgumentException("Ошибка. Попробуйте еще раз.");
}
System.err.printf("%s.%s()%n", MySingleton.class.getName(), MySingleton.class.getSimpleName());
}
Легко убедиться, что наша реализация позволяет корректно поймать и обработать исключение, выброшенное из конструктора. В случае ошибки статическое поле accessor не будет изменено, и наш Singleton останется в исходном непроинициализированном состоянии.
Посмотрим, насколько эффективно сможет откомпилировать наш класс Hotspot JVM. Запустим Java с опциями -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining. Получаем вот такую диагностику:
@ 20 ru.habrahabr.Singleton::getInstance (9 bytes) inline (hot)
@ 3 ru.habrahabr.Singleton$DummyFunction::get (5 bytes) accessor
Я не очень разбираюсь в сообщениях JIT-компилятора, но по-моему, это самое быстрое, что можно получить из метода getInstance(). Вместо него подставлен метод accessor, возвращающий значение одного из полей.
Подведем итоги
+ Гарантирован 1 экземпляр класса Singleton
+ Безопасность при многопоточном использовании
+ Ленивая инициализация
+ Обработка ошибок конструктора
+ Эффективная реализация
- Вызов виртуального метода при получении экземпляра Singleton
- Громоздкий код
- 3 дополнительных класса: интерфейс и 2 реализации
Links
[1] Реализация Singleton в JAVA
[2] Правильный Singleton в Java
[3] Singleton pattern
[4] What is an efficient way to implement a singleton pattern in Java?
[5] The «Double-Checked Locking is Broken» Declaration
[6] Singleton Design Pattern