Случайный разговор: как объяснить моей девушке, что такое шаблон синглтона?

задняя часть Шаблоны проектирования
Случайный разговор: как объяснить моей девушке, что такое шаблон синглтона?

Приближаются выходные, приближается День труда 1 мая Моя девушка еще не решила, куда пойти поиграть, и все еще присматривается к различным стратегиям. Я читал книгу рядом со мной и молча огорчил мой бумажник. Вдруг моя девушка начала спрашивать:

Что такое синглтон

Одноэлементный шаблон, также известный как шаблон монады, является широко используемым шаблоном проектирования программного обеспечения.При применении этого шаблона класс одноэлементного объекта должен гарантировать, что существует только один экземпляр.

Во многих случаях всей системе требуется только один глобальный объект, который помогает нам координировать поведение системы в целом. Например, в серверной программе информация о конфигурации сервера хранится в файле, и данные конфигурации единообразно считываются одноэлементным объектом, а затем другие объекты в процессе обслуживания получают информацию о конфигурации через одноэлементный объект. Такой подход упрощает управление конфигурацией в сложных средах.

Чтобы привести простой пример, как и в системе моногамии в Китае, муж и жена могут быть только один на один, то есть мужчина может иметь только одну жену одновременно. Такая ситуация называется синглтоном. В Китае моногамия ограничена Законом о браке.

男女双方来到民政局登记
if 男方目前已经有老婆{
    提醒二位无法结婚。并告知其当前老婆是谁。
}else{
    检查女方婚姻状况,其他基本信息核实。
    同意双方结为夫妻。
}

Для разработки кода класс, который одновременно имеет только один объект-экземпляр, называется синглтоном. Итак, как гарантировать, что класс может иметь только один объект?

мы знаем, что вобъектно-ориентированныймысль, черезКонструкторы классов могут создавать объекты, пока памяти достаточно, вы можете создать любое количество объектов.

Поэтому, если вы хотите ограничить класс только одним одноэлементным объектом, вам нужно хорошенько поработать над его конструктором.

Идея реализации паттерна object singleton такова:

1. Класс может возвращать ссылку на объект (всегда одну и ту же) и метод для получения экземпляра (должен быть статическим методом, обычно использующим имя getInstance);

2. Когда мы вызываем этот метод, если ссылка, хранящаяся в классе, не равна нулю, ссылка возвращается.Если ссылка, содержащаяся в классе, равна нулю, создается экземпляр класса, и ссылка экземпляра присваивается ссылка, принадлежащая классу;

3. В то же время мы также определяем конструктор класса как частный метод, чтобы код в другом месте не мог создавать экземпляр объекта класса, вызывая конструктор класса, а класс можно было получить только через статический метод. метод, предоставляемый классом единственный экземпляр .

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  

    public static Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}  

Приведенный выше код Java реализует простой одноэлементный шаблон. Мы определяем конструктор как закрытый, а затем предоставляем метод getInstance, который определяет, существует ли уже экземпляр класса, и напрямую возвращает значение, если он существует. Если он не существует, создайте его и вернитесь.

потокобезопасный синглтон

Для параллелизма вы можете обратиться к«Как объяснить параллелизм и параллелизм своей девушке».

В Китае, чтобы иметь жену, и мужчины, и женщины должны обратиться в Бюро по гражданским делам со своими соответствующими регистрационными книгами для получения сертификата. Сотрудники бюро по гражданским делам сначала проверят семейное положение обеих сторон в системе, а затем пройдут процедуры регистрации. Предпосылка, что успешная регистрация моногамии может быть гарантирована, состоит в том, что не будет проблем с параллелизмом.

Если предположить, что мужчина может зарегистрироваться одновременно с двумя разными женщинами, то существует вероятность того, что на запрос персонала он не женат, и тогда его могут зарегистрировать для заключения брака дважды. Конечно, в реальной жизни это просто невозможно.

Однако в программах, когда есть многопоточный сценарий, такая ситуация встречается очень часто. Так же, как код выше.

Если два потока выполняют строку кода if (instance==null) одновременно, решение будет принято, и затем каждый из них выполнит instance = new Singleton(); и каждый вернет экземпляр, и будет сгенерировано несколько экземпляров в настоящее время нет гарантированного синглтона!

Реализация вышеупомянутого синглтона обычно называется режимом ленивого человека, так называемый ленивый человек означает, что объект будет генерироваться только тогда, когда объект нужен (метод getInstance будет генерироваться при вызове метода). Это немного похоже на ситуацию в реальной жизни, когда «сырой рис готовится до зрелости», и только когда вы должны выйти замуж, вы начинаете получать сертификат.

Описанный выше ленивый режим не является потокобезопасным, поэтому его не рекомендуется использовать для ежедневной разработки. На основе этого шаблона мы можем реализовать потокобезопасный синглтон следующим образом:

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}  

Добавляя synchronized в метод getInstance, проблема параллелизма решается за счет блокировок. Эта реализация позволяет избежать проблемы создания нескольких объектов.

замок с двойной проверкой

Приведенный выше потокобезопасный ленивый способ записи может хорошо работать в нескольких потоках, но, к сожалению, такой подход неэффективен, т.к. управление параллелизмом требуется только при первой инициализации, в большинстве случаев синхронизация не требуется.

На самом деле мы можем оптимизировать приведенный выше код, потому что ленивый режим использует synchronized для определения метода синхронизации, как мы знаем,Synchronized также можно использовать для определения блоков синхронизированного кода, а степень детализации блоков синхронизированного кода меньше, чем у синхронизированных методов, поэтому эффективность будет выше.Например, следующий код:

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}  

В приведенной выше форме объект создается путем блокировки только тогда, когда singleton == null.Если singleton!=null, он может быть возвращен напрямую, и контроль параллелизма отсутствует. Значительно улучшена эффективность.

Как видно из вышеприведенного кода, фактически во всем процессе выносятся два одноэлементных == нулевых суждения, поэтому этот метод называется «блокировка с двойной проверкой».

Также стоит отметить, что,При реализации блокировки с двойной проверкой статическая переменная-член singleton должна быть изменена с помощью volatile, чтобы обеспечить атомарность ее инициализации, в противном случае на нее может ссылаться неинициализированный объект.

Почему блокировки с двойной проверкой должны использовать volatile для украшения статической переменной-члена singleton? Почему это не нужно потокобезопасным неряхам? В следующей статье этот вопрос будет подробно рассмотрен.

режим голодного человека

Ленивый режим, упомянутый выше, на самом деле является практикой ленивой загрузки мышления. Эта реализация имеет большое преимущество, то есть она создается только тогда, когда она фактически используется. Если она не используется, она никогда не будет создана. ненужные накладные расходы.

Однако у этого подхода на самом деле есть небольшой недостаток, то есть при его первом использовании требуется операция инициализации, которая может занять относительно много времени. Если известно, что будет использоваться некий объект, то фактически можно использовать голодный метод реализации.

Так называемый голодный человек должен приготовить его заранее и дать вам сразу, когда вам это нужно. Это более распространенное ежедневное «купить билет, а затем сесть в автобус» и пройти обычные процедуры.

Например, следующий код, голодный режим:

public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return instance;  
    }  
}   

Или следующий код, вариант Hungry Man:

public class Singleton {  
    private Singleton instance = null;  
    static {  
    instance = new Singleton();  
    }  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return this.instance;  
    }  
}  

На самом деле, нет существенной разницы между двумя приведенными выше фрагментами кода, оба из которых представляют собой экземпляры объектов класса посредством статики.Статические переменные в режиме Hunger инициализируются при загрузке класса. Блоки статического кода в варианте Hungry также выполняются вместе с загрузкой класса.

Вышеупомянутые два метода фактически определяют статические переменные-члены, чтобы гарантировать, что экземпляр может быть создан при инициализации класса.

Поскольку инициализация класса выполняется ClassLoader, фактически используется механизм безопасности потоков ClassLoader. Метод loadClass ClassLoader использует ключевое слово synchronized при загрузке классов. Из-за этого, если не переопределить, этот метод синхронизируется (потокобезопасный) по умолчанию на протяжении всего процесса загрузки.

Помимо двух вышеперечисленных голодных китайских методов, есть еще один метод реализации, который также реализован с помощью инициализации calss, то есть синглтона, реализованного через статические внутренние классы:

public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
}  

В голодном режиме, упомянутом ранее, пока загружен класс Singleton, будет создан экземпляр.

Таким образом загружается класс Singleton, и экземпляр не обязательно инициализируется. Поскольку класс SingletonHolder активно не используется, только при вызове метода getInstance класс SingletonHolder будет отображаться для создания экземпляра экземпляра.

Использование статических внутренних классов и использование загрузчика классов для обеспечения безопасности потоков, это похоже на голодный режим, но он имеет функцию ленивой загрузки, которая учитывает ленивый режим, что имеет большие преимущества по сравнению с ним.

Уничтожение синглтона

Как упоминалось выше, важной предпосылкой для реализуемого нами синглтона является установка конструктора в качестве закрытого метода, чтобы избежать внешних вызовов. Однако неужели нельзя вызвать приватный конструктор извне?

Не совсем,Мы можем вызывать приватные методы в классе через отражение, конструктор не является исключением, поэтому мы можем уничтожить синглтон через отражение.

В дополнение к этой ситуации есть еще одна ситуация, которую легко упустить из виду, то есть сериализация и десериализация объектов также уничтожит синглтон.

Например, при использовании ObjectInputStream для десериализации в процессе генерации объекта readObject из ObjectInputStream будет вызываться конструктор без аргументов для создания нового объекта путем отражения.

Поэтому при сериализации и десериализации одноэлементных объектов обязательно учитывайте тот факт, что одноэлементный объект может быть уничтожен.

Эту проблему можно решить, определив readResolve в классе Singleton:

/**
 * 使用双重校验锁方式实现单例
 */
public class Singleton implements Serializable{
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    private Object readResolve() {
        return singleton;
    }
} 

Перечисление реализует синглтон

В StakcOverflow обсуждается вопрос о том, как эффективно реализовать одноэлементный шаблон в Java?:

Как показано выше, ответ с наибольшим количеством голосов: используйте перечисление.

Ответчик процитировал точку зрения Джошуа Блоха, четко выраженную в «Эффективной Java»:

Метод реализации синглетонов с помощью перечислений не получил широкого распространения, но одноэлементные типы перечисления стали лучшим способом реализации синглтонов.

Если вы действительно понимаете использование синглтона и некоторых возможных ям, вы можете прийти к такому же выводу, а именно: использование перечисления для реализации синглтона — хороший способ.

Перечисление реализует синглтон:

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}  

Выше реализован очень простой синглтон, по количеству строк кода он компактнее любого из представленных ранее, а также потокобезопасен.

На самом деле этого недостаточно, чтобы убедить нас в оптимальности этого метода. Но есть еще одна важная причина, а именно:Перечисления решают проблему десериализации, нарушающей синглтоны

Для получения этих знаний вы можете обратиться к«Почему я рекомендую вам использовать перечисление для реализации синглетонов»В этой статье подробно описаны все точки знаний о перечислении и синглтоне.

Реализация синглтона без синхронизированного

Все методы, упомянутые выше, если они потокобезопасны, на самом деле прямо или косвенно используют синхронизацию Итак, если вы не можете использовать синхронизацию, как реализовать синглтон?

Использовать блокировку? Это конечно возможно, но по факту он все равно вообще заблокирован.Есть ли способ без блокировки?

Ответ - да, это CAS. CAS — это оптимистичная технология блокировки. Когда несколько потоков пытаются использовать CAS для одновременного обновления одной и той же переменной, только один поток может обновить значение переменной, а другие потоки терпят неудачу. Неудачный поток не будет приостановлен, но будет сказал проиграть это соревнование и может попробовать еще раз.

Недавно добавленный java.util.concurrent (J.U.C) в JDK1.5 основан на CAS. По сравнению с алгоритмом блокировки синхронизированного, CAS является обычной реализацией неблокирующего алгоритма. Таким образом, J.U.C значительно улучшил производительность.

Реализовать одноэлементный паттерн с помощью CAS (AtomicReference):

public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>(); 

    private Singleton() {}

    public static Singleton getInstance() {
        for (;;) {
            Singleton singleton = INSTANCE.get();
            if (null != singleton) {
                return singleton;
            }

            singleton = new Singleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

Преимущество использования CAS заключается в том, что ему не нужно использовать традиционный механизм блокировки для обеспечения безопасности потоков.CAS представляет собой алгоритм, основанный на ожидании занятости, который опирается на реализацию базового оборудования.По сравнению с блокировками, он не требует дополнительного потребления. переключения и блокировки потоков и может поддерживать высокую степень параллелизма.

Использование CAS для реализации синглетонов — это всего лишь идея, просто для расширения, чтобы помочь читателям усвоить знания о CAS и синглтонах, а не использовать их в коде! ! ! Этот код на самом деле имеет много возможностей для оптимизации. Умник, ты знаешь о скрытых опасностях приведенного выше кода?