Думаю, все слышали о паттерне singleton и даже много писали. Это также один из самых проверенных паттернов проектирования на собеседовании. Интервьюер часто просит написать два типа паттернов singleton и объяснить их принципы. Без лишних слов, давайте научимся хорошо отвечать на этот вопрос интервью.
Что такое одноэлементный шаблон
Когда интервьюер спрашивает, что такое одноэлементный шаблон, не отвечайте на вопрос и дайте ответ, как будто существует два типа одноэлементного шаблона, и начните с определения одноэлементного шаблона.
Шаблон Singleton относится к шаблону проектирования, в котором объекты создаются в памяти только один раз. Когда один и тот же объект используется в программе несколько раз и имеет одну и ту же функцию, чтобы предотвратить парение памяти из-за частого создания объектов, режим singleton позволяет программе создать в памяти только один объект, чтобы все места, которые необходимо вызвать, совместно используют этот одноэлементный объект.
Типы одноэлементных паттернов
Существует два типа одноэлементных шаблонов:
-
懒汉式
: создавайте объект класса singleton только тогда, когда вам действительно нужно использовать объект. -
饿汉式
: одноэлементный объект был создан при загрузке класса и ожидает использования программой.
Ленивое создание одноэлементных объектов
Ленивый способ создания объекта состоит в том, чтобы сначала определить, был ли создан экземпляр объекта (пустой), прежде чем программа использует объект, и, если он был создан, вернуть объект этого класса напрямую. В противном случае сначала выполняется операция создания экземпляра.
В соответствии с приведенной выше блок-схемой можно написать следующий код
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
Да, здесь мы написали очень хороший паттерн singleton, но он не идеален, но это не влияет на наше использование этого «объекта singleton».
Выше приведен ленивый способ создания одноэлементного объекта, позже я объясню, где этот код можно оптимизировать и в чем заключаются проблемы.
Голодный китайский стиль для создания одноэлементного объекта
голодный китаец类加载
Объект был создан при вызове программы, а объект singleton может быть возвращен непосредственно при вызове программы, то есть мы указали, что хотим создать этот объект сразу при кодировании, и нам не нужно ждать, пока он призван создать его.
Что касается загрузки классов, то это связано с содержимым JVM, мы можем просто думать, что объект-одиночка был создан при запуске программы.
public class Singleton{
private static final Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance() {
return singleton;
}
}
Обратите внимание, что приведенный выше код создал экземпляр объекта Singleton в памяти в строке 3, и не будет нескольких экземпляров объекта Singleton.
При загрузке класса в куче памяти создается объект Singleton, а при выгрузке класса объект Singleton также умирает.
Как ленивый человек гарантирует создание только одного объекта
Давайте вернемся к основному методу Lazy Man.
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
На самом деле у этого метода есть проблема: представьте, если два потока одновременно определят, что синглтон пуст, то они оба будут создавать экземпляр объекта Singleton, который становится множественным кейсом. Итак, что нам нужно решить, так это线程安全
проблема.
Самое простое решение — заблокировать метод или заблокировать объект класса, и программа будет выглядеть так:
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
// 或者
public static Singleton getInstance() {
synchronized(Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
return singleton;
}
Это позволяет избежать риска одновременного создания объектов Singleton двумя потоками, но приводит к другой проблеме:Каждый раз, когда вы хотите получить объект, вам нужно сначала получить блокировку, а производительность параллелизма очень низкая.В крайних случаях может возникнуть феномен зависания.
Следующее, что нужно сделать, это优化性能
: Цель состоит в том, чтобы заблокировать и создать, если нет экземпляра объекта.Если он уже был создан, нет необходимости блокировать и напрямую получать экземпляр
Таким образом, метод прямого добавления блокировок к методу отменяется, потому что этот метод в любом случае должен сначала получить блокировку.
public static Singleton getInstance() {
if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new Singleton();
}
}
}
return singleton;
}
Вышеприведенный код решил это отличноПараллельная безопасность + неэффективность производительностипроблема:
- В строке 2, если синглтон не пуст, объект возвращается напрямую без получения блокировки, и если несколько потоков обнаруживают, что синглтон пуст, они войдут в ветвь;
- В 3-й строке кода несколько потоков пытаются конкурировать за одну и ту же блокировку, успешно конкурирует только один поток, и первый поток, который получает блокировку, будет судить, является ли синглтон снова пустым, поскольку экземпляр синглтона мог быть создан предыдущим нить
- Другие потоки, которые получают блокировку, позже выполняют код проверки в строке 4 и обнаруживают, что синглтон больше не пуст, тогда он не будет создавать другой объект, а просто вернет объект напрямую.
- После этого все потоки, входящие в этот метод, не получат блокировку, и он больше не будет пустым, когда одноэлементный объект оценивается в первый раз.
Поскольку его нужно оценить дважды, а объект класса заблокирован, этот метод ленивой записи также называется:Двойная проверка + блокировка
Полный код выглядит так:
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance() {
if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new Singleton();
}
}
}
return singleton;
}
}
Приведенный выше код почти идеален, но есть одна последняя проблема: перестановка инструкций.
Используйте volatile для предотвращения перестановки инструкций
Создание объекта проходит в три этапа в JVM:
(1) Выделить место в памяти для синглтона
(2) Инициализировать одноэлементный объект
(3) Укажите синглетон на выделенное пространство памяти
Переупорядочивание инструкций означает:При условии, что окончательный результат гарантированно будет правильным, JVM может выполнять операторы вне порядка кодирования программы, чтобы максимально повысить производительность программы.
На этих трех шагах может произойти перестановка инструкций на шагах 2 и 3, и порядок создания объектов станет 1-3-2, что приведет к получению объектов несколькими потоками.Возможно, что поток A находится в процессе создания объектов. ., после выполнения шагов 1 и 3 поток B определяет, что синглетон не пуст, и если он получает неинициализированный объект-синглтон, он сообщит об исключении NPE. Текст более неясен, вы можете увидеть блок-схему:
Использование ключевого слова volatile может предотвратить переупорядочивание инструкций, принцип более сложный, эта статья не будет расширяться, его можно понять так:Переменные, измененные с помощью ключевого слова volatile, могут гарантировать, что порядок выполнения их инструкций соответствует порядку, заданному программой, и никакого изменения порядка не произойдет., чтобы исключения NPE не возникали в многопоточной среде.
У Volatile есть и вторая функция: переменные, модифицированные ключевым словом volatile, могут обеспечить его видимость в памяти, то есть значение переменной, считываемое потоком в каждый момент, является последним значением в памяти, и поток каждый раз оперирует переменной Оба должны сначала прочитать переменную.
Окончательный код выглядит так:
public class Singleton {
private static volatile Singleton singleton;
private Singleton(){}
public static Singleton getInstance() {
if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new Singleton();
}
}
}
return singleton;
}
}
Уничтожение ленивых и голодных одиночек
Будь то идеальный ленивый человек или голодный человек, его невозможно победитьотражение и сериализация, оба из которых могут уничтожать одноэлементные объекты (создавать несколько объектов).
использоватьотражениесломать одноэлементный шаблон
Вот пример использования отражения для разрушения одноэлементного шаблона.
public static void main(String[] args) {
// 获取类的显式构造器
Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();
// 可访问私有构造器
construct.setAccessible(true);
// 利用反射构造新对象
Singleton obj1 = construct.newInstance();
// 通过正常方式获取单例对象
Singleton obj2 = Singleton.getInstance();
System.out.println(obj1 == obj2); // false
}
Приведенный выше код попадает в самую точку: используйте отражение, чтобы принудительно получить доступ к частному конструктору класса для создания другого объекта.
использоватьСериализация и десериализациясломать одноэлементный шаблон
Вот пример нарушения одноэлементного шаблона с помощью сериализации и десериализации.
public static void main(String[] args) {
// 创建输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
// 将单例对象写到文件中
oos.writeObject(Singleton.getInstance());
// 从文件中读取单例对象
File file = new File("Singleton.file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
// 判断是否是同一个对象
System.out.println(newInstance == Singleton.getInstance()); // false
}
Причина, по которой адреса двух объектов не равны, заключается в том, что когда метод readObject() читает объект, он должен вернуть новый экземпляр объекта, который должен указывать на новый адрес памяти.
Реализация перечисления, которая заставляет интервьюера аплодировать
Мы освоили общепринятое написание слов «ленивый» и «голодный», и этого обычно достаточно. Однако как же нам, стремящимся к конечному, остановиться на этом?В книге "Эффективная Java" дано окончательное решение.Нечего и говорить, узнав следующее, интервьюеру действительно стоит проверить вас.
После JDK 1.5 существует еще один способ реализации одноэлементного шаблона с использованием языка Java:枚举
Полный код для перечисления для реализации одноэлементного шаблона выглядит следующим образом:
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("这是枚举类型的单例模式!");
}
}
Реализация одноэлементного шаблона с использованием перечисления имеет три преимущества по сравнению с двумя другими реализациями, давайте рассмотрим их подробнее.
Преимущество 1: Код с первого взгляда
Код более лаконичный по сравнению с голодным и ленивым. Одноэлементный шаблон может быть завершен как минимум 3 строками кода:
public enum Test {
INSTANCE;
}
Начнем с самого интуитивного места: когда мы впервые увидим эти 3 строчки кода, мы почувствуем少
, да, меньше.Хотя это преимущество немного надумано, но чем меньше кода вы пишете, тем меньше вероятность, что вы сделаете ошибку.
Преимущество 2: Естественная безопасность потоков и одноэлементность
Ему не нужно выполнять никаких дополнительных операций для обеспечения единственности объекта и безопасности потоков.
Я написал кусок тестового кода и поместил его ниже, этот кусок кода может доказать, что при запуске программы создается только один объект Singleton, и он потокобезопасен.
Мы можем просто понять процесс перечисления и создания экземпляра: при запуске программы будет вызван конструктор нулевого параметра Singleton, объект Singleton будет создан и назначен INSTANCE, и он больше никогда не будет создан.
public enum Singleton {
INSTANCE;
Singleton() { System.out.println("枚举创建对象了"); }
public static void main(String[] args) { /* test(); */ }
public void test() {
Singleton t1 = Singleton.INSTANCE;
Singleton t2 = Singleton.INSTANCE;
System.out.print("t1和t2的地址是否相同:" + t1 == t2);
}
}
// 枚举创建对象了
// t1和t2的地址是否相同:true
В дополнение к преимуществу 1 и преимуществу 2, есть еще одно преимущество, которое保护单例模式
, что делает перечисление уже в текущем поле шаблона singleton无懈可击
охватывать
Преимущество 3: перечисление защищает одноэлементный шаблон от нарушения
Использование перечисления не позволяет вызывающему абоненту использоватьОтражение, сериализация и десериализацияЭтот механизм вызывает создание нескольких одноэлементных объектов, нарушая одноэлементный шаблон.
антибликовое
Класс перечисления по умолчанию наследует класс Enum.При использовании отражения для вызова newInstance() он будет судить, является ли класс классом перечисления, и если да, то будет выдано исключение.
Запретить десериализацию создания нескольких объектов перечисления
При чтении объекта Singleton каждый тип перечисления и имя перечисления уникальны, поэтому при сериализации просто выведите тип перечисления и имя переменной в файл и десериализуйте прочитанный файл в объект, используйте метод valueOf(String name) класса Класс Enum для поиска соответствующего объекта перечисления по имени переменной.
Поэтому в процессе сериализации и десериализации записывается и читается только тип и имя перечисления, а над объектом нет никакой операции.
резюме:
(1) Внутреннее использование класса EnumОпределение типа перечисленияПредотвращение создания нескольких объектов с помощью отражения
(2) Класс Enum сериализует (десериализует) объекты, записывая (считывая) тип объекта и имя перечисления,Сопоставление имен перечислений с помощью метода valueOf()Найти единственный экземпляр объекта в памяти, предотвращая создание нескольких объектов путем десериализации
(3) Классы перечисления не должны обращать внимание на безопасность потоков, уничтожение одиночных элементов и проблемы с производительностью, поскольку время создания объектов отличается отГолодные синглтоны в китайском стиле имеют ту же цель.
Суммировать
(1) Существует два распространенных способа написания одноэлементного шаблона: ленивый и голодный.
(2) Стиль ленивого человека: экземпляр объекта создается только тогда, когда объект необходимо использовать.Правильный метод реализации: двойная проверка + блокировка, который решает проблемы безопасности параллелизма и низкой производительности.
(3) Голодный китайский стиль: одноэлементный объект был создан при загрузке класса, и объект может быть возвращен непосредственно при получении одноэлементного объекта, и не будет проблем с безопасностью параллелизма и производительностью.
(4) Если во время разработки требования к памяти очень высоки, используйте метод ленивой записи для создания объекта в определенное время;
(5) Если требования к памяти не высоки, используйте голодный метод письма на китайском языке, потому что он прост и не подвержен ошибкам, и нет параллельных проблем с безопасностью и производительностью.
(6) Чтобы переменные не сообщали о NPE из-за переупорядочения инструкций в многопоточной среде, необходимо добавить ключевое слово volatile в объект singleton, чтобы предотвратить переупорядочение инструкций.
(7) Самый элегантный способ реализации — использовать перечисление, его код упрощен, нет проблем с безопасностью потоков, а класс Enum внутренне предотвращает уничтожение синглетонов во время отражения и десериализации.