При просмотре кода компании я обнаружил, что в проекте много применений одноэлементного паттерна, и два найденных одноэлементных паттерна реализованы по-разному.Так сколько же способов написать одноэлементный паттерн? Паттерн singleton кажется очень простым, но при написании у него много проблем.
План этой статьи
- Что такое одноэлементный шаблон
- Голодный китайский стиль для создания одноэлементного объекта
- Ленивое создание одноэлементных объектов
- Преимущества и недостатки одноэлементного шаблона
- Сценарии применения одноэлементного паттерна
Что такое одноэлементный шаблон
Убедитесь, что в классе есть толькоэкземпляр,исамовоспроизводящийсяИ предоставьте этот экземпляр всей системе, и есть два метода создания: один - создание в стиле голодного человека, а другой - создание в стиле ленивого человека.
Голодный китайский стиль для создания одноэлементного шаблона
Создание в голодном стиле означает, что объекты создаются при загрузке класса, а не тогда, когда они необходимы.
public class HungrySingleton {
private static HungrySingleton hungrySingleton = new HungrySingleton();
/**
* 私有构造函数,不能被外部所访问
*/
private HungrySingleton() {}
/**
* 返回单例对象
* */
public static HungrySingleton getHungrySingleton() {
return hungrySingleton;
}
}
инструкция:
- Конструктор приватизирован, чтобы гарантировать, что внешний вызов не может вызвать конструктор для создания объекта, а поведение создания объекта может быть определено только этим классом.
- только через
getHungrySingletonметод получения объекта -
HungrySingletonОбъект создан [создан при загрузке класса]
недостаток:
- если
getHungrySingletonНе использовался, пустая трата ресурсов
преимущество:
- Зависит от
ClassLoadГарантированная безопасность потока
Одноэлементный шаблон ленивой сборки
Ленивое создание создает объект в первый раз, когда он нужен
-
Существует неправильный ленивый способ создания одноэлементного объекта.
По определению его легко модифицировать на основе голодного китайского стиля вышеpublic class LazySingleton { private static LazySingleton lazySingleton = null; /** * 构造函数私有化 * */ private LazySingleton() { } private static LazySingleton getLazySingleton() { if (lazySingleton == null) { return new LazySingleton(); } return lazySingleton; } }инструкция:
- Конструкторская приватизация
- при необходимости
getLazySingletonсоздается при вызове метода]
Ну вроде проблем нет, но когда несколько потоков вызывают одновременноgetLazySingletonметод, объект в это время не инициализируется, и два потока проходят через него одновременно.lazySingleton == null, создаст дваLazySingletonобъект. должен что-то сделатьgetLazySingletonметод является потокобезопасным
-
synchronizeилиLock
Легко думать об использованииsynchronizeилиLockзаблокировать метод
использоватьsynchronize:public class LazySynchronizeSingleton { private static LazySynchronizeSingleton lazySynchronizeSingleton= null; /** * 构造函数私有化 * */ private LazySynchronizeSingleton() { } public synchronized static LazySynchronizeSingleton getLazySynchronizeSingleton() { if (lazySynchronizeSingleton == null) { lazySynchronizeSingleton = new LazySynchronizeSingleton(); } return lazySynchronizeSingleton; } }использовать
Lock:public class LazyLockSingleton { private static LazyLockSingleton lazyLockSingleton = null; /** * 锁 **/ private static Lock lock = new ReentrantLock(); /** * 构造函数私有化 * */ private LazyLockSingleton() { } public static LazyLockSingleton getLazyLockSingleton() { try { lock.lock(); if (lazyLockSingleton == null) { lazyLockSingleton = new LazyLockSingleton(); } } finally { lock.unlock(); } return lazyLockSingleton; } }Хотя эти два метода обеспечивают безопасность потоков, производительность низкая, поскольку небезопасность потоков в основном вызвана этим кодом:
if (lazyLockSingleton == null) { lazyLockSingleton = new LazyLockSingleton(); }Блокировка метода приведет к блокировке потока независимо от того, был ли объект инициализирован. если объект
nullБлокировка выполняется только тогда, когда объект неnullКогда блокировка не выполняется, производительность будет улучшена.Двойная проверка блокировки может выполнить это требование. -
проверка двойного замка
Судить перед блокировкойlazyDoubleCheckSingleton == nullУстановлен ли он, если не установлен, вернуть созданный объект напрямую, установленный в замке
public class LazyDoubleCheckSingleton {
/**
* 使用volatile进行修饰,禁止指令重排
* */
private static volatile LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
/**
* 构造函数私有化
* */
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getLazyDoubleCheckSingleton() {
if (lazyDoubleCheckSingleton == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazyDoubleCheckSingleton == null) {
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
инструкция:
- Почему вам нужно
lazyDoubleCheckSingletonДобавить кvolatileмодификатор
так какlazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();Не атомарный, в три шага:- за
lazyDoubleCheckSingletonВыделить память - Вызвать конструктор для инициализации
- будет
lazyDoubleCheckSingletonОбъект указывает на выделенную память [выполнить этот шагlazyDoubleCheckSingletonне будетnull】
lazyDoubleCheckSingletonне дляnull, затем поток 2 выполняется дляif (lazyDoubleCheckSingleton == null), поток 2 может напрямую вернуть неправильно инициализированныйlazyDoubleCheckSingletonобъект. Основной причиной ошибки являетсяlazyDoubleCheckSingletonНеправильная инициализация завершена [запись], но другие потоки прочиталиlazyDoubleCheckSingletonЗначение [читать], использоватьvolatileПереупорядочивание инструкций можно отключить, и операция чтения не будет вызываться до операции записи через барьер памяти [executeif (lazyDoubleCheckSingleton == null)】 - за
недостаток:
-
Чтобы обеспечить потокобезопасность, код не выглядит элегантно и слишком раздут.
-
статический внутренний класс
public class LazyStaticSingleton { /** * 静态内部类 * */ private static class LazyStaticSingletonHolder { private static LazyStaticSingleton lazyStaticSingleton = new LazyStaticSingleton(); } /** * 构造函数私有化 * */ private LazyStaticSingleton() { } public static LazyStaticSingleton getLazyStaticSingleton() { return LazyStaticSingletonHolder.lazyStaticSingleton; } }Статические внутренние классы инициализируются при вызове, поэтому они ленивы,
LazyStaticSingleton lazyStaticSingleton = new LazyStaticSingleton();Вроде голоден, но только зовgetLazyStaticSingletonОн будет инициализирован только тогда, когда безопасность потокаClassLoadГарантия, не думайте о том, как заблокировать
Хотя предыдущие методы реализации синглетонов имеют свои преимущества и недостатки, в основном они удовлетворяют требованиям безопасности одноэлементных потоков. Однако всегда находятся люди, которые не привыкли к преимуществам одноэлементного режима и нападают на него. Атака на него — не что иное, как создание более чем одного класса,javaОбъекты создаются вnew,clone, сериализация, отражение. Конструктор приватизации не позволяет создавать объекты через new, а класс singleton не реализованCloneableИнтерфейс не может пройтиcloneметод создания объекта, единственными оставшимися атаками являются атаки отражения и атаки сериализации
Рефлекторная атака:
public class ReflectAttackTest {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//静态内部类
LazyStaticSingleton lazyStaticSingleton = LazyStaticSingleton.getLazyStaticSingleton();
//通过反射创建LazyStaticSingleton
Constructor<LazyStaticSingleton> constructor = LazyStaticSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazyStaticSingleton lazyStaticSingleton1 = constructor.newInstance();
//打印结果为false,说明又创建了一个新对象
System.out.println(lazyStaticSingleton == lazyStaticSingleton1);
//synchronize
LazySynchronizeSingleton lazySynchronizeSingleton = LazySynchronizeSingleton.getLazySynchronizeSingleton();
Constructor<LazySynchronizeSingleton> lazySynchronizeSingletonConstructor = LazySynchronizeSingleton.class.getDeclaredConstructor();
lazySynchronizeSingletonConstructor.setAccessible(true);
LazySynchronizeSingleton lazySynchronizeSingleton1 = lazySynchronizeSingletonConstructor.newInstance();
System.out.println(lazySynchronizeSingleton == lazySynchronizeSingleton1);
//lock
LazyLockSingleton lazyLockSingleton = LazyLockSingleton.getLazyLockSingleton();
Constructor<LazyLockSingleton> lazyLockSingletonConstructor = LazyLockSingleton.class.getDeclaredConstructor();
lazyLockSingletonConstructor.setAccessible(true);
LazyLockSingleton lazyLockSingleton1 = lazyLockSingletonConstructor.newInstance();
System.out.println(lazyLockSingleton == lazyLockSingleton1);
//双重锁检查
LazyDoubleCheckSingleton lazyDoubleCheckSingleton = LazyDoubleCheckSingleton.getLazyDoubleCheckSingleton();
Constructor<LazyDoubleCheckSingleton> lazyDoubleCheckSingletonConstructor = LazyDoubleCheckSingleton.class.getDeclaredConstructor();
lazyDoubleCheckSingletonConstructor.setAccessible(true);
LazyDoubleCheckSingleton lazyDoubleCheckSingleton1 = lazyDoubleCheckSingletonConstructor.newInstance();
System.out.println(lazyDoubleCheckSingleton == lazyDoubleCheckSingleton1);
}
}
Существуют атаки отражения, и можно создать новый объект, а результаты печатиfalse. В соответствии с идеями, предоставленными в Интернете, мы спасем существующую атаку отражения, Спасательная позиция выглядит следующим образом:
private LazySynchronizeSingleton() {
//flag为线程间共享,进行加锁控制
synchronized (LazySynchronizeSingleton.class) {
if (flag == false) {
flag = !flag;
} else {
throw new RuntimeException("单例模式被攻击");
}
}
}
Конструктор можно вызвать только один раз, второй вызов вызовет исключение,flagчтобы определить, был ли конструктор уже вызван один раз. Но мы все еще можем изменить его путем отраженияflagЗначение:
//调用反射前将flag设置为false
Field flagField = lazySynchronizeSingleton.getClass().getDeclaredField("flag");
flagField.setAccessible(true);
flagField.set(lazySynchronizeSingleton, false);
Неудачное спасение, вы можете пройтиfinalМодификацию модифицировать запрещено, но сначала можно убрать отражениеfinal, добавивfinalИзмените значение.Для рефлексивных атак бессильно.Вы можете выбрать только метод создания singleton, который не применяется к рефлексивным атакам.
Атака десериализации:
public class SerializableAttackTest {
public static void main(String[] args) {
//懒汉式
HungrySingleton hungrySingleton = HungrySingleton.getHungrySingleton();
//序列化
byte[] serialize = SerializationUtils.serialize(hungrySingleton);
//反序列化
HungrySingleton hungrySingleton1 = SerializationUtils.deserialize(serialize);
System.out.println(hungrySingleton == hungrySingleton1);
//双重锁
LazyDoubleCheckSingleton lazyDoubleCheckSingleton = LazyDoubleCheckSingleton.getLazyDoubleCheckSingleton();
byte[] serialize1 = SerializationUtils.serialize(lazyDoubleCheckSingleton);
LazyDoubleCheckSingleton lazyDoubleCheckSingleton11 = SerializationUtils.deserialize(serialize1);
System.out.println(lazyDoubleCheckSingleton == lazyDoubleCheckSingleton11);
//lock
LazyLockSingleton lazyLockSingleton = LazyLockSingleton.getLazyLockSingleton();
byte[] serialize2 = SerializationUtils.serialize(lazyLockSingleton);
LazyLockSingleton lazyLockSingleton1 = SerializationUtils.deserialize(serialize2);
System.out.println(lazyLockSingleton == lazyLockSingleton1);
//synchronie
LazySynchronizeSingleton lazySynchronizeSingleton = LazySynchronizeSingleton.getLazySynchronizeSingleton();
byte[] serialize3 = SerializationUtils.serialize(lazySynchronizeSingleton);
LazySynchronizeSingleton lazySynchronizeSingleton1 = SerializationUtils.deserialize(serialize3);
System.out.println(lazySynchronizeSingleton == lazySynchronizeSingleton1);
//静态内部类
LazyStaticSingleton lazyStaticSingleton = LazyStaticSingleton.getLazyStaticSingleton();
byte[] serialize4 = SerializationUtils.serialize(lazySynchronizeSingleton);
LazyStaticSingleton lazyStaticSingleton1 = SerializationUtils.deserialize(serialize4);
System.out.println(lazyStaticSingleton == lazyStaticSingleton1);
}
}
Результат печати такойfalse, есть атаки десериализации
Для атак десериализации все еще существуют эффективные методы спасения.
private Object readResolve() {
return lazySynchronizeSingleton;
}
Добавить кreadResolveметод и вернуть созданный объект singleton.Что касается принципа спасения, вы можете следитьSerializationUtils.deserializeКод известен
Приведенный выше метод реализации одноэлементных объектов должен учитывать как безопасность потоков, так и атаки, а создание одноэлементных объектов с помощью перечисления вообще не должно волновать эти проблемы.
- перечислить
Реализация кода тоже довольно красивая, всего лишьpublic enum EnumSingleton { INSTANCE; public static EnumSingleton getEnumSingleton() { return INSTANCE; } }8поколение
Принцип реализации: Поле класса перечисления фактически является экземпляром объекта соответствующего типа перечисления.
Вы можете обратиться к:implementing-singleton-with-an-enum-in-java
Тест атаки перечислением:
Атака отражения вызовет исключение, атака сериализации для нее недействительна, а результат печатиpublic class EnumAttackTest { public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { EnumSingleton enumSingleton = EnumSingleton.getEnumSingleton(); //序列化攻击 byte[] serialize4 = SerializationUtils.serialize(enumSingleton); EnumSingleton enumSingleton2 = SerializationUtils.deserialize(serialize4); System.out.println(enumSingleton == enumSingleton2); //反射攻击 Constructor<EnumSingleton> enumSingletonConstructor = EnumSingleton.class.getDeclaredConstructor(); enumSingletonConstructor.setAccessible(true); EnumSingleton enumSingleton1 = enumSingletonConstructor.newInstance(); System.out.println(enumSingleton == enumSingleton1); } }true, создание одноэлементного объекта с перечислением действительно неуязвимо
Преимущества одноэлементного паттерна
- Создается только один экземпляр, что экономит память.
- Уменьшите нагрузку на систему, создание объектов и повторное использование объектов оказывают определенное влияние на производительность.
- Избегайте многократного занятия ресурсов
- Устанавливайте глобальные точки доступа в системе, оптимизируйте и оптимизируйте общие ресурсы
Подводя итог, это экономит ресурсы и повышает производительность.
Недостатки шаблона Singleton
- Не применимо к изменяющимся объектам
- В одноэлементном шаблоне нет уровня абстракции, и его трудно расширять.
- Противоречит единому принципу. Класс должен реализовывать только одну логику, независимо от того, является ли он синглтоном или нет, это должен решать бизнес.
Сценарии применения одноэлементного паттерна
-
Spring IOCПо умолчанию создается с использованием шаблона singleton.bean - При создании объекта потребляется слишком много ресурсов
- Среда, в которой необходимо определить большое количество статических констант и статических методов, таких как классы инструментов [ощущение, что это наиболее распространенный сценарий приложения]
резюме
Всего представлено шесть способов корректного создания одноэлементного объекта.Рекомендуется использовать голодный китайский стиль для создания одноэлементного объекта.Если есть требование по использованию ресурсов, рекомендуется использовать статический внутренний класс [остерегайтесь атаки десериализации] и другие методы гарантированы.Безопасность потоков также будет влиять на производительность. Класс перечисления на самом деле очень хороший, потокобезопасный, нет атаки отражения и атаки десериализации, но я чувствую, что этот метод создания синглтона используется меньше, а код компании использует проверку двойной блокировки и статические внутренние классы Атака сериализации] Создайте метод синглтона, даже когда интервьюер попросил меня написать синглтон, когда я вышел на собеседование, я использовал метод перечисления, а интервьюер не знал, что есть такой метод
Наконец прикрепил:Полный пример кода + тестовый код
Добро пожаловатьforkиstar