При просмотре кода компании я обнаружил, что в проекте много применений одноэлементного паттерна, и два найденных одноэлементных паттерна реализованы по-разному.Так сколько же способов написать одноэлементный паттерн? Паттерн 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