Дан Java-фрагмент, демонстрирующий паттерн Singleton: public class Singleton { private static Singleton instance; private Singleton(){} public static Singleton getInstance(){ if (instance == null) instance = new Singleton(); return instance; } } — объясните проблему этого варианта в многопоточной среде и предложите как минимум два безопасных варианта реализации Singleton с объяснением плюсов и минусов
Ваша реализация не потокобезопасна. Если два потока одновременно вызовут getInstance и увидят instance == null, оба могут создать по одному объекту — нарушается инвариант Singleton.Кроме того, без дополнительных гарантий возможны тонкие проблемы с видимостью кэш−память,реорганизацияинструкцийкэш-память, реорганизация инструкцийкэш−память,реорганизацияинструкций, особенно при частичной инициализации объекта в слабых моделях памяти встарыхреализацияхJVMв старых реализациях JVMвстарыхреализацияхJVM.
Ниже — несколько безопасных вариантов с разбором плюсов/минусов.
1) Простая синхронизация метода Код: public class Singleton { private static Singleton instance; private Singleton {} public static synchronized Singleton getInstance { if instance==nullinstance == nullinstance==null instance = new Singleton; return instance; } } Плюсы:
Очень просто, корректно в любых версиях Java.Гарантирует единственность и видимость. Минусы:Синхронизация на каждый вызов getInstance — потенциальная потеря производительности узкоеместоузкое местоузкоеместо при частых обращениях.
2) Double-checked locking сvolatileс volatileсvolatile — эффективный ленивый вариант Код требуетJava5+требует Java 5+требуетJava5+: public class Singleton { private static volatile Singleton instance; private Singleton {} public static Singleton getInstance { if instance==nullinstance == nullinstance==null { synchronized Singleton.classSingleton.classSingleton.class { if instance==nullinstance == nullinstance==null instance = new Singleton; } } return instance; } } Плюсы:
Ленивое создание создаетсяприпервомобращениисоздается при первом обращениисоздаетсяприпервомобращении.Накладные расходы на синхронизацию только при первой инициализации; дальше — быстрые чтения. Минусы:Более сложен, требует volatile; до Java 5 этот паттерн был некорректен из‑за реорганизации инструкций.Сложнее для понимания/поддержки.
3) Initialization-on-demand holder рекомендуемыйленивыйподходрекомендуемый ленивый подходрекомендуемыйленивыйподход
Код: public class Singleton { private Singleton {} private static class Holder { static final Singleton INSTANCE = new Singleton; } public static Singleton getInstance { return Holder.INSTANCE; } } Плюсы:
Ленивое создание, безопасно в многопоточности инициализациястатическоговложенногоклассавыполняетсяJVMсинхронноинициализация статического вложенного класса выполняется JVM синхронноинициализациястатическоговложенногоклассавыполняетсяJVMсинхронно.Нет явной синхронизации и накладных расходов на доступ.Прост и надежен. Минусы:Немного менее очевидный приём для новичков ноширокоиспользуемибезопасенно широко используем и безопасенноширокоиспользуемибезопасен.
4) Eager initialization жаднаяинициализацияжадная инициализацияжаднаяинициализация
Код: public class Singleton { private static final Singleton INSTANCE = new Singleton; private Singleton {} public static Singleton getInstance { return INSTANCE; } } Плюсы:
Очень прост, потокобезопасен статическаяинициализацияJVMстатическая инициализация JVMстатическаяинициализацияJVM.Никакой синхронизации в рантайме. Минусы:Объект создается при загрузке класса, даже если он никогда не будет использован неленивыйне ленивыйнеленивый.
5) Enum рекомендуется,еслиподходитсемантикерекомендуется, если подходит семантикерекомендуется,еслиподходитсемантике
Код: public enum Singleton { INSTANCE; // поля и методы } Плюсы:
Самый простой и безопасный способ: корректен в многопоточности, защищает от сериализации несоздастновыйэкз.придесериализациине создаст новый экз. при десериализациинесоздастновыйэкз.придесериализации и устойчив к большинству рефлексивных атак.Является рекомендуемым паттерном для singleton в Effective Java JoshuaBlochJoshua BlochJoshuaBloch. Минусы:Не подходит, если Singleton должен наследоваться от какого‑то другого класса enumуженаследуетjava.lang.Enumenum уже наследует java.lang.Enumenumуженаследуетjava.lang.Enum.Некоторым разработчикам стиль enum может показаться непривычным для «обычного» объекта.
Дополнительные замечания
Сериализация: если вы используете обычный класс, учтите, что при десериализации может создаваться новый экземпляр; можно реализовать readResolve, чтобы вернуть единственный экземпляр.Рефлексия: через Constructor#setAccessibletruetruetrue можно попытаться создать ещё один экземпляр; enum защищает от этого лучше. Для обычного класса можно в конструкторе проверять, не был ли уже создан экземпляр, и бросать исключение.Выбор: если нужна ленивость и максимальная простота — Initialization-on-demand holder; если важна защита от сериализации/рефлексии — enum; если система старше Java 5 — используйте синхронизированный метод илистатическуюинициализациюили статическую инициализациюилистатическуюинициализацию.
Если нужно, могу показать пример защиты от сериализации/рефлексии для конкретной реализации или провести сравнение производительности в вашем окружении.
Проблема исходного варианта в многопоточной среде
Ваша реализация не потокобезопасна. Если два потока одновременно вызовут getInstance и увидят instance == null, оба могут создать по одному объекту — нарушается инвариант Singleton.Кроме того, без дополнительных гарантий возможны тонкие проблемы с видимостью кэш−память,реорганизацияинструкцийкэш-память, реорганизация инструкцийкэш−память,реорганизацияинструкций, особенно при частичной инициализации объекта в слабых моделях памяти встарыхреализацияхJVMв старых реализациях JVMвстарыхреализацияхJVM.Ниже — несколько безопасных вариантов с разбором плюсов/минусов.
1) Простая синхронизация метода
Очень просто, корректно в любых версиях Java.Гарантирует единственность и видимость.Код:
public class Singleton {
private static Singleton instance;
private Singleton {}
public static synchronized Singleton getInstance {
if instance==nullinstance == nullinstance==null instance = new Singleton;
return instance;
}
}
Плюсы:
Минусы:Синхронизация на каждый вызов getInstance — потенциальная потеря производительности узкоеместоузкое местоузкоеместо при частых обращениях.
2) Double-checked locking сvolatileс volatileсvolatile — эффективный ленивый вариант
Ленивое создание создаетсяприпервомобращениисоздается при первом обращениисоздаетсяприпервомобращении.Накладные расходы на синхронизацию только при первой инициализации; дальше — быстрые чтения.Код требуетJava5+требует Java 5+требуетJava5+:
public class Singleton {
private static volatile Singleton instance;
private Singleton {}
public static Singleton getInstance {
if instance==nullinstance == nullinstance==null {
synchronized Singleton.classSingleton.classSingleton.class {
if instance==nullinstance == nullinstance==null instance = new Singleton;
}
}
return instance;
}
}
Плюсы:
Минусы:Более сложен, требует volatile; до Java 5 этот паттерн был некорректен из‑за реорганизации инструкций.Сложнее для понимания/поддержки.
3) Initialization-on-demand holder рекомендуемыйленивыйподходрекомендуемый ленивый подходрекомендуемыйленивыйподход Код:
Ленивое создание, безопасно в многопоточности инициализациястатическоговложенногоклассавыполняетсяJVMсинхронноинициализация статического вложенного класса выполняется JVM синхронноинициализациястатическоговложенногоклассавыполняетсяJVMсинхронно.Нет явной синхронизации и накладных расходов на доступ.Прост и надежен.public class Singleton {
private Singleton {}
private static class Holder {
static final Singleton INSTANCE = new Singleton;
}
public static Singleton getInstance {
return Holder.INSTANCE;
}
}
Плюсы:
Минусы:Немного менее очевидный приём для новичков ноширокоиспользуемибезопасенно широко используем и безопасенноширокоиспользуемибезопасен.
4) Eager initialization жаднаяинициализацияжадная инициализацияжаднаяинициализация Код:
Очень прост, потокобезопасен статическаяинициализацияJVMстатическая инициализация JVMстатическаяинициализацияJVM.Никакой синхронизации в рантайме.public class Singleton {
private static final Singleton INSTANCE = new Singleton;
private Singleton {}
public static Singleton getInstance {
return INSTANCE;
}
}
Плюсы:
Минусы:Объект создается при загрузке класса, даже если он никогда не будет использован неленивыйне ленивыйнеленивый.
5) Enum рекомендуется,еслиподходитсемантикерекомендуется, если подходит семантикерекомендуется,еслиподходитсемантике Код:
Самый простой и безопасный способ: корректен в многопоточности, защищает от сериализации несоздастновыйэкз.придесериализациине создаст новый экз. при десериализациинесоздастновыйэкз.придесериализации и устойчив к большинству рефлексивных атак.Является рекомендуемым паттерном для singleton в Effective Java JoshuaBlochJoshua BlochJoshuaBloch.public enum Singleton {
INSTANCE;
// поля и методы
}
Плюсы:
Минусы:Не подходит, если Singleton должен наследоваться от какого‑то другого класса enumуженаследуетjava.lang.Enumenum уже наследует java.lang.Enumenumуженаследуетjava.lang.Enum.Некоторым разработчикам стиль enum может показаться непривычным для «обычного» объекта.
Дополнительные замечания
Сериализация: если вы используете обычный класс, учтите, что при десериализации может создаваться новый экземпляр; можно реализовать readResolve, чтобы вернуть единственный экземпляр.Рефлексия: через Constructor#setAccessibletruetruetrue можно попытаться создать ещё один экземпляр; enum защищает от этого лучше. Для обычного класса можно в конструкторе проверять, не был ли уже создан экземпляр, и бросать исключение.Выбор: если нужна ленивость и максимальная простота — Initialization-on-demand holder; если важна защита от сериализации/рефлексии — enum; если система старше Java 5 — используйте синхронизированный метод илистатическуюинициализациюили статическую инициализациюилистатическуюинициализацию.Если нужно, могу показать пример защиты от сериализации/рефлексии для конкретной реализации или провести сравнение производительности в вашем окружении.