Повний гайд по шаблону проектування Java Singleton
Шаблон проектування синглтон в Java є одним з найбільш фундаментальних і широко обговорюваних шаблонів у розробці програмного забезпечення, особливо в колекції шаблонів проектування Gang of Four. Цей шаблон створення відповідає конкретній, але поширеній вимозі в розробці додатків — забезпечення існування лише одного екземпляра певного класу протягом усього життєвого циклу додатка, одночасно забезпечуючи глобальний доступ до цього екземпляра.
Шаблон Java Singleton виходить далеко за межі очевидної простоти, відіграючи вирішальну роль в управлінні спільними ресурсами, координації дій та підтримці стабільного стану в сучасних Java-додатках.
Розуміння Java Singleton стає важливим у міру зростання складності додатків. У цій статті розглянуті підходи до реалізації, компроміси та найкращі практики для прийняття обґрунтованих рішень.
Які основні принципи шаблону Singleton?
Шаблон Java Singleton працює на основі трьох основних принципів, які визначають його поведінку та вимоги до реалізації. Ці принципи працюють разом, щоб забезпечити досягнення шаблоном своєї мети, зберігаючи при цьому належну інкапсуляцію та механізми контрольованого доступу.
Перший принцип стосується обмеження екземплярів, що означає, що клас синглтон в Java повинен запобігати створенню зовнішнім кодом декількох екземплярів через звичайне виклик конструктора. Це обмеження зазвичай досягається шляхом надання конструктору класу приватного статусу, що ефективно блокує будь-які спроби створити екземпляр класу за допомогою стандартного оператора new ззовні самого класу.
Другий принцип зосереджується на підтримці єдиного екземпляра протягом усього життєвого циклу програми. Клас повинен внутрішньо керувати саме одним екземпляром себе, який зазвичай зберігається як приватна статична змінна. Цей екземпляр служить канонічним представленням класу і повинен ретельно управлятися, щоб запобігти створенню додаткових екземплярів за допомогою різних механізмів.
Третій принцип встановлює вимоги до глобального доступу, вимагаючи, щоб клас Singleton надавав публічний статичний метод, який служить точкою входу для отримання єдиного екземпляра. Цей метод діє як контрольований шлюз, гарантуючи, що весь зовнішній код отримує доступ до одного й того самого екземпляра, зберігаючи цілісність поведінки синглтона.
Як реалізувати підхід до завчасного ініціювання?
Завчасне ініціювання є найпростішим підходом до реалізації java-синглтона, де єдиний екземпляр створюється відразу після першого завантаження класу віртуальною машиною Java. Цей підхід надає пріоритет простоті та безпеці потоків над ефективністю використання ресурсів та можливостями відкладеного завантаження.
Наведений нижче приклад java-одиночного екземпляра демонструє підхід до активної ініціалізації з повними деталями реалізації:
package com.example.singleton;
public class EagerInitializedSingleton {
private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
private EagerInitializedSingleton() {
// Private constructor prevents external instantiation
}
public static EagerInitializedSingleton getInstance() {
return instance;
}
}
Активна ініціалізація забезпечує простоту та вбудовану безпеку потоків, оскільки екземпляри створюються під час завантаження класу, що усуває проблеми синхронізації та умови гонки. Це ідеально підходить для легких синглтонів, які обов'язково будуть використовуватися. Однак екземпляри створюються незалежно від фактичного використання, що може призвести до марнування ресурсів під час управління дорогими ресурсами, такими як підключення до бази даних або великі структури даних.
Що таке метод ініціалізації статичного блоку?
Ініціалізація статичного блоку розширює попередню ініціалізацію, додаючи обробку винятків, зберігаючи при цьому ранній час інстанціювання. Вона створює екземпляр синглтона в статичному блоці під час завантаження класу, забезпечуючи більший контроль для обробки винятків конструктора, таких як підключення до бази даних або читання файлу конфігурації.
package com.example.singleton;
public class StaticBlockSingleton {
private static StaticBlockSingleton instance;
private StaticBlockSingleton() {
// Private constructor prevents external instantiation
}
static {
try {
instance = new StaticBlockSingleton();
} catch (Exception e) {
throw new RuntimeException("Exception occurred in creating singleton instance", e);
}
}
public static StaticBlockSingleton getInstance() {
return instance;
}
}
Підхід до ініціалізації статичного блоку забезпечує кращу обробку помилок, ніж попередня ініціалізація, зберігаючи гарантії безпеки потоків. Він дозволяє використовувати складну логіку ініціалізації, витончену обробку виключень і значущі повідомлення про помилки, коли створення екземпляру синглтона Java не вдається.
Незважаючи на ці переваги, він має ті ж фундаментальні обмеження, що й активна ініціалізація — екземпляри створюються під час завантаження класу незалежно від використання, що може спричинити непотрібне споживання ресурсів і довше завантаження дорогих об'єктів-одинаків.
Як працює лінива ініціалізація на практиці?
Відкладена ініціалізація відкладає створення екземпляра до першого виклику getInstance, вирішуючи проблему марнування ресурсів при активній ініціалізації шляхом створення екземплярів тільки тоді, коли це необхідно. Це покращує час запуску і зменшує використання пам'яті. Реалізація перевіряє, чи екземпляр є нульовим при кожному виклику, створюючи нові екземпляри за необхідності або повертаючи існуючі. Безпека потоків вимагає ретельного розгляду в багатопотокових середовищах.
package com.example.singleton;
public class LazyInitializedSingleton {
private static LazyInitializedSingleton instance;
private LazyInitializedSingleton() {
// Private constructor prevents external instantiation
}
public static LazyInitializedSingleton getInstance() {
if (instance == null) {
instance = new LazyInitializedSingleton();
}
return instance;
}
}
Основною перевагою лінивої ініціалізації є ефективність використання ресурсів, оскільки екземпляри синглтонів створюються тільки за потреби. Це є цінним для синглтонів, які керують дорогими ресурсами або складними процедурами ініціалізації, які не завжди можуть бути необхідними.
Однак кілька потоків можуть одночасно перевіряти умову null і створювати окремі екземпляри, порушуючи принцип синглтонів Java. Ця умова гонки робить базову ліниву ініціалізацію непридатною для багатопотокових додатків без додаткових механізмів синхронізації.
Чому нам потрібні потокобезпечні реалізації синглтонів?
Потокобезпека стає критично важливою проблемою при реалізації шаблону Java-синглтонів у багатопотокових Java-додатках. Без належної синхронізації кілька потоків можуть потенційно створити окремі екземпляри того, що повинно бути класом синглтонів, що призведе до прихованих помилок, конфліктів ресурсів і порушення основних принципів шаблону синглтонів.
Проблема потокової безпеки виникає в першу чергу на етапі створення екземпляра. Коли кілька потоків одночасно отримують доступ до методу getInstance і виявляють, що екземпляр не існує, вони можуть всі продовжити створювати свої власні екземпляри.
package com.example.singleton;
public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton() {
// Private constructor prevents external instantiation
}
public static synchronized ThreadSafeSingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
return instance;
}
}
Метод synchronized забезпечує безпеку потоків, дозволяючи одночасно виконувати getInstance тільки одному потоку. Хоча це запобігає виникненню гонок, але створює навантаження на продуктивність, оскільки кожен виклик повинен отримувати і звільняти блокування, навіть якщо екземпляр існує.
Більш складний підхід використовує подвійне блокування, щоб мінімізувати навантаження на синхронізацію, зберігаючи безпеку потоків:
public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
if (instance == null) {
synchronized (ThreadSafeSingleton.class) {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
Шаблон подвійної перевірки блокування зменшує навантаження на синхронізацію, виконуючи дорогий синхронізований блок тільки тоді, коли екземпляр є нульовим. Перша перевірка відбувається поза синхронізованим блоком для підвищення продуктивності, а друга перевірка всередині блоку гарантує, що тільки один потік створює екземпляр, навіть якщо кілька потоків одночасно проходять першу перевірку.
Що таке підхід Білла Пью до синглтонів?
Реалізація синглтонів Білла Пью, відома як «ідіома утримувача ініціалізації на вимогу», є елегантним і ефективним підходом до потокобезпечних синглтонів у Java. Він використовує механізм завантаження класів Java для досягнення лінивої ініціалізації без явної синхронізації, поєднуючи ліниве завантаження з перевагами потокової безпеки.
Підхід використовує приватний статичний вкладений клас, що містить екземпляр синглтона. Вкладений клас завантажується тільки при першому зверненні getInstance, забезпечуючи створення синглтона тільки при необхідності, зберігаючи потокову безпеку завдяки гарантіям завантаження класів JVM.
package com.example.singleton;
public class BillPughSingleton {
private BillPughSingleton() {
// Private constructor prevents external instantiation
}
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
Підхід Білла Пью забезпечує справжню ліниву ініціалізацію, оскільки SingletonHelper завантажується тільки при першому виклику getInstance. Він є внутрішньо безпечним для потоків завдяки синхронізації завантаження класів JVM з чудовою продуктивністю і без накладних витрат на синхронізацію після початкового завантаження.
Ця реалізація широко вважається найкращою практикою для реалізації java singleton, оскільки вона поєднує в собі ефективність, безпеку потоків і ліниву ініціалізацію без складності подвійної перевірки блокування.
Як рефлексія може порушити шаблони синглтонів?
Рефлексія становить значну загрозу цілісності шаблону синглтонів, надаючи механізми для обходу обмеження приватного конструктора, яке є основою реалізації синглтонів. За допомогою API рефлексії зовнішній код може отримати доступ до приватних конструкторів, створити кілька екземплярів і повністю порушити контракт синглтону без будь-яких попереджень під час компіляції або очевидних індикаторів під час виконання.
Атака рефлексією працює шляхом отримання об'єкта Class для класу синглтона, вилучення його оголошених конструкторів (включаючи приватні), надання їм доступу, а потім виклику їх для створення нових екземплярів. Цей приклад Java-синглтона демонструє, як рефлексія може повністю обійти звичайні засоби контролю доступу, що запобігають створенню декількох екземплярів:
package com.example.singleton;
import java.lang.reflect.Constructor;
public class ReflectionSingletonTest {
public static void main(String[] args) {
EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
EagerInitializedSingleton instanceTwo = null;
try {
Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
constructor.setAccessible(true);
instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
}
}
При виконанні цього тесту хеш-коди instanceOne та instanceTwo будуть різними, що свідчить про створення двох окремих екземплярів класу, який, як передбачається, є синглтоном.
Чому варто розглянути синглтон enum?
Підхід синглтон enum, який пропагує Джошуа Блох у книзі «Effective Java», забезпечує найнадійніше рішення завдяки використанню вбудованих механізмів enum у Java. Він усуває вразливості рефлексії, забезпечуючи при цьому вбудовану безпеку потоків та підтримку серіалізації без додаткової складності.
Java enums є за своєю суттю синглтонами — JVM гарантує, що кожна константа enum буде інстанційована лише один раз, і запобігає атакам на основі рефлексії. Конструктори enum є неявними приватними і не можуть бути викликані через рефлексію, що робить enum singletons імунними до вразливостей рефлексії.
package com.example.singleton;
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
// Implement singleton functionality here
}
}
Впровадження синглтона переліку є надзвичайно лаконічним, забезпечуючи при цьому всебічний захист від поширених вразливостей синглтона. Константа INSTANCE представляє екземпляр синглтона і доступна з будь-якого місця в додатку за допомогою EnumSingleton.INSTANCE. До переліку можна додавати методи, щоб забезпечити функціональність, яка зазвичай асоціюється з класами синглтона.
Enum singletons автоматично правильно обробляють серіалізацію, зберігаючи властивості singleton протягом циклів серіалізації та десеріалізації без необхідності використання додаткових методів readResolve або іншого коду, специфічного для серіалізації. Ця вбудована підтримка серіалізації усуває значне джерело помилок і складності в розподілених додатках.
Що відбувається з серіалізацією та синглетоном?
Серіалізація створює значні проблеми для реалізації синглетонів, оскільки стандартна серіалізація Java створює нові екземпляри під час десеріалізації, порушуючи цілісність контракту синглетона. Десеріалізація за замовчуванням генерує окремі об'єкти, а не зберігає канонічне посилання на синглетон.
Це відбувається тому, що серіалізація Java обходить інстанціювання на основі конструктора, використовуючи альтернативні механізми створення об'єктів, які ігнорують обмеження синглетона. Додатки з серіалізованими синглтонами ризикують ненавмисним розмноженням екземплярів, що спричиняє тонкі аномалії під час виконання та невідповідності в поведінці, які ставлять під загрозу надійність системи.
package com.example.singleton;
import java.io.Serializable;
public class SerializedSingleton implements Serializable {
private static final long serialVersionUID = -7604766932017737115L;
private SerializedSingleton() {
// Private constructor prevents external instantiation
}
private static class SingletonHelper {
private static final SerializedSingleton instance = new SerializedSingleton();
}
public static SerializedSingleton getInstance() {
return SingletonHelper.instance;
}
}
To demonstrate the serialization problem, consider this test case:
package com.example.singleton;
import java.io.*;
public class SingletonSerializedTest {
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
SerializedSingleton instanceOne = SerializedSingleton.getInstance();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream("filename.ser"));
out.writeObject(instanceOne);
out.close();
ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser"));
SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
in.close();
System.out.println("instanceOne hashCode=" + instanceOne.hashCode());
System.out.println("instanceTwo hashCode=" + instanceTwo.hashCode());
}
}
The solution to the serialization problem involves implementing the readResolve method, which allows classes to control what object is returned during deserialization:
protected Object readResolve() {
return getInstance();
}
Коли реалізовано readResolve, механізм серіалізації викликає цей метод замість створення нового екземпляра, дозволяючи синглетону повернути існуючий екземпляр і зберегти цілісність синглетона через межі серіалізації.
BlueVPS — надійна інфраструктура для Java-додатків
При розгортанні Java-додатків, що реалізують складні шаблони проектування, такі як Singleton, надійна інфраструктура хостингу стає необхідною для збереження цілісності шаблону та продуктивності. BlueVPS надає веб-хостинг VPS корпоративного рівня, забезпечуючи стабільну продуктивність Java-додатків у різних середовищах. Наша платформа забезпечує обчислювальну надійність, необхідну для професійної розробки Java, незалежно від того, чи йдеться про розгортання складних реалізацій синглтонів, чи про розподілені системи, що вимагають надійних можливостей серіалізації.
Висновок
Шаблон проектування Singleton залишається важливим компонентом професійної розробки Java, незважаючи на складність реалізації та проблеми проектування. Оптимальний вибір реалізації вимагає оцінки вимог, специфічних для додатка, включаючи паралельність, час ініціалізації ресурсів та сумісність серіалізації. Реалізація Білла Пью та підходи на основі переліків є найкращими практиками в галузі, забезпечуючи чудову продуктивність та зменшуючи типові вразливості архітектури.
Blog