Вы действительно понимаете шаблон singleton?

Шаблоны проектирования

На собеседованиях нас часто спрашивают: "Вы знакомы с шаблоном singleton? Пожалуйста, напишите реализацию шаблона singleton? Каковы применения шаблона singleton...". Вопросов о шаблоне singleton много, и они очень часто встречаются в интервью.

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

выполнить

Диаграмма классов выглядит следующим образом:

singleton
Интерпретация:

  • Идея реализации шаблона singleton:
    • В классе есть собственная переменная (эта переменная может быть создана при ее использовании, а может быть создана до ее использования);
    • Убедитесь, что глобально существует только один экземпляр переменной;
    • Предоставьте общедоступный метод для доступа к переменной извне;
  • Сравнивая приведенные выше идеи реализации, можно увидеть, что этапы реализации делятся на три этапа, которые имеют следующие характеристики:
    • Частная статическая переменная: добавьте ключевое слово static, эквивалентное константе, отражающей уникальную особенность экземпляра;
    • Приватизация конструктора: цель состоит в том, чтобы не позволить другим классам выполнять новую операцию, то есть не позволять другим классам создавать класс, то есть гарантировать, что глобально существует только один экземпляр переменной;
    • Обеспечьте статическую глобальную точку доступа: чтобы получить доступ к частным переменным класса, поскольку частные переменные изменяются с помощью ключевого слова static, открытый метод для получения переменной также должен быть статически изменен;

Ленивый - поток небезопасен

код реализации

public class LazySingleton {
    // 构造函数私有化
    private LazySingleton(){
        System.out.println("当前线程名称: " + Thread.currentThread().getName() + "\t 我是构造方法...");
    }

    // 私有的静态变量
    private static LazySingleton lazySingleton;

    // 提供静态的全局访问点
    public static LazySingleton getSingleton() {
        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        System.out.println(lazySingleton); // 打印当前对象的唯一标识
        return lazySingleton;
    }
}

тестовый код

public class TestSingletons {
    public static void main(String[] args) {
        // 单线程场景下,直接调用
        LazySingleton.getSingleton();
        LazySingleton.getSingleton();
        LazySingleton.getSingleton();
        LazySingleton.getSingleton();

        // 多线程场景
        /*for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                LazySingleton.getSingleton();
            }, String.valueOf(i)).start();
        }*/
    }
}
  1. Откройте код однопоточного сценария и прокомментируйте код многопоточного сценария:
  • Результаты приведены ниже:
当前线程名称: main	 我是构造方法...
singleton.LazySingleton@4554617c
singleton.LazySingleton@4554617c
singleton.LazySingleton@4554617c
singleton.LazySingleton@4554617c
  • Анализ результатов: Получается, что весь процесс строится только один раз, и уникальный идентификатор этой переменной равен4554617c, проиллюстрировать单线程场景下是没有问题的.
  1. Откройте код многопоточного сценария и прокомментируйте код однопоточного сценария:
  • Результаты приведены ниже:
当前线程名称: 2	 我是构造方法...
当前线程名称: 8	 我是构造方法...
当前线程名称: 4	 我是构造方法...
singleton.LazySingleton@ae526cf
当前线程名称: 6	 我是构造方法...
当前线程名称: 0	 我是构造方法...
singleton.LazySingleton@134f6cee
当前线程名称: 3	 我是构造方法...
当前线程名称: 9	 我是构造方法...
singleton.LazySingleton@6e154e44
当前线程名称: 5	 我是构造方法...
当前线程名称: 7	 我是构造方法...
当前线程名称: 1	 我是构造方法...
singleton.LazySingleton@2fd04fd1
singleton.LazySingleton@67c084e5
singleton.LazySingleton@47e3e4b5
singleton.LazySingleton@1b9c704e
singleton.LazySingleton@21279f82
singleton.LazySingleton@2ceb2de
singleton.LazySingleton@14550b42
  • Анализ результатов: Результаты многократных прогонов противоречивы, а уникальные идентификаторы экземпляров различаются, то есть, если экземпляр создается десять раз, каждый раз будет генерироваться новый экземпляр. Это показывает, что реализация находится в многопоточном сценарии.无法保证线程安全的.

Ленивый человек — безопасность потоков

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

код реализации

public class LazySafeSingleton {
    // 构造方法私有化
    private LazySafeSingleton(){
        System.out.println("当前线程名称: " + Thread.currentThread().getName() + "\t 我是构造方法...");
    }

    // 私有的静态变量
    private static LazySafeSingleton lazySafeSingleton;

    // 提供同步的静态全局访问点
    public synchronized static LazySafeSingleton getSingleton() {
        if (lazySafeSingleton == null) {
            lazySafeSingleton = new LazySafeSingleton();
        }
        System.out.println(lazySafeSingleton); // 打印当前对象的唯一标识
        return lazySafeSingleton;
    }
}

Между этим методом и первым есть только одно отличие: в предоставленную глобальную точку доступа, то есть публичный метод для получения экземпляра объекта, добавляется блокировка синхронизации, чтобы гарантировать, что только один поток может получить доступ к методу getSingleton() в то же время.

Дальнейшее чтение:Подробное объяснение синхронизации Java

тестовый код

public class TestSingletons {
    public static void main(String[] args) {
        // 单线程场景
        LazySafeSingleton.getSingleton();
        LazySafeSingleton.getSingleton();
        LazySafeSingleton.getSingleton();
        LazySafeSingleton.getSingleton();

        // 多线程场景
        /*for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                LazySafeSingleton.getSingleton();
            }, String.valueOf(i)).start();
        }*/
    }
}
  1. Однопоточный сценарий:
  • результат операции:
当前线程名称: main	 我是构造方法...
singleton.LazySafeSingleton@4554617c
singleton.LazySafeSingleton@4554617c
singleton.LazySafeSingleton@4554617c
singleton.LazySafeSingleton@4554617c
  • Анализ результатов: Он создается только один раз, и в однопоточных сценариях проблем не возникает.
  1. В многопоточном сценарии:
  • результат операции:
当前线程名称: 0	 我是构造方法...
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
  • Анализ результатов: После нескольких прогонов вы обнаружите, что он будет построен только один раз.

Проверка двойной блокировки — потокобезопасность

Анализ предыдущей реализации метода getSingleton() с декорированием ключевого слова synchronized хотя и может гарантировать одновременный доступ только к одному потоку, обеспечивая согласованность в многопоточных сценариях, но это также принесет еще одну проблему: параллелизм уменьшать. Так, тогда правильно懒汉式-线程安全Делайте улучшения.

код реализации

public class DCLSingleton {
    // 构造方法私有化
    private DCLSingleton(){
        System.out.println("当前线程名称: " + Thread.currentThread().getName() + "\t 我是构造方法...");
    }

    // 静态私有变量
    private volatile static DCLSingleton dclSingleton;

    // 提供静态全局访问点
    public static DCLSingleton getDclSingleton() {
        if (dclSingleton == null) {
            synchronized (DCLSingleton.class) {
                if (dclSingleton == null) {
                    dclSingleton = new DCLSingleton();
                }
            }
        }
        System.out.println(dclSingleton); // 打印当前对象的唯一标识
        return dclSingleton;
    }
}

Сравнивая предыдущую реализацию, вы обнаружите два отличия:

  1. добавлено в приватную переменнуюvolatileключевые слова. Причина в следующем:dclSingleton = new DCLSingleton();После компиляции в байт-код он делится на три шага:
1. 为 dclSingleton 分配内存空间
2. 初始化 dclSingleton
3. 将 dclSingleton 执行分配的内存地址

Поскольку JVM имеет характеристики перестановки инструкций, в多线程环境下就可能会出现一个线程获取到的实例还未被初始化的情况. Например, поток T1 выполняет 1 и 3. В это время поток T2 вызывает метод getDclSingleton() и обнаруживает, что dclSingleton не пуст, поэтому dclSingleton будет возвращен, но dclSingleton в это время не инициализирован. Таким образом, добавление ключевого слова volatile при объявлении статических закрытых переменных гарантирует, что JVM не сможет выполнять перестановку инструкций, тем самым решая вышеуказанные проблемы.

  1. Исходный метод синхронизации становится синхронизированным блоком.
if (dclSingleton == null) {
    synchronized (DCLSingleton.class) {
        dclSingleton = new DCLSingleton();
    }
}

В коде только с одним, если в условиях многопоточности, предполагая, что поток T1 и поток T2 одновременно вводят оператор dclSingleton == null, тогда один из T1 или T2 выполнит dclSingleton = new DCLSingleton(); Освободите блокировку, и другой поток снова выполнит оператор dclSingleton = new DCLSingleton();, что приведет к двойному выполнению конструктора, поэтому в блоке синхронизированного кода необходимо определить, пуст ли снова dclSingleton.

тестовый код

public class TestSingletons {
    public static void main(String[] args) {
        // 单线程
        DCLSingleton.getDclSingleton();
        DCLSingleton.getDclSingleton();
        DCLSingleton.getDclSingleton();
        DCLSingleton.getDclSingleton();

        // 多线程
        /*for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                LazySafeSingleton.getSingleton();
            }, String.valueOf(i)).start();
        }*/
    }

}
  1. В однопоточном сценарии:
  • результат операции:
当前线程名称: main	 我是构造方法...
singleton.DCLSingleton@4554617c
singleton.DCLSingleton@4554617c
singleton.DCLSingleton@4554617c
singleton.DCLSingleton@4554617c
  • Анализ результатов: Конструктор выполняется только один раз, и в однопоточных сценариях проблем не возникает.
  1. В многопоточном сценарии:
  • результат операции:
当前线程名称: 0	 我是构造方法...
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
  • Анализ результатов: После нескольких прогонов вы обнаружите, что он будет построен только один раз.

Hungry Man — безопасность потоков

Основное различие между ленивым стилем и голодным стилем заключается в том, что статические приватные переменные ленивого стиля пусты и конструируются при их использовании, тогда как голодный стиль уже конструируется при загрузке, то есть он был сконструирован раньше. использовать.这种方式会造成一定的资源浪费。

public class HungrySingleton {
    // 构造方法私有化
    private HungrySingleton() {
        System.out.println("当前线程名称: " + Thread.currentThread().getName() + "\t 我是构造方法...");
    }

    // 静态私有变量
    private static HungrySingleton hungrySingleton = new HungrySingleton();

    // 提供静态全局访问点
    public static HungrySingleton getSingleton() {
        System.out.println(hungrySingleton); // 打印当前对象的唯一标识
        return hungrySingleton;
    }
}

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

другие методы

статический внутренний класс

Улучшив реализацию Hungry-Thread Safety, можно разделить два этапа создания экземпляра объекта и использования экземпляра объекта, то есть создание выполняется при использовании реализации. Статические внутренние классы подходят идеально.

public class InnerClazzSingleton {
    // 私有化构造方法
    private InnerClazzSingleton(){}

    // 静态内部类,保证使用时才加载
    private static class InnerClassSingletonHolder {
        private static final InnerClazzSingleton SINGLETON = new InnerClazzSingleton();
    }

    // 提供静态全局访问点
    public static InnerClazzSingleton getInstance() {
        return InnerClassSingletonHolder.SINGLETON;
    }
}

Этот подход использует тот факт, что статические внутренние классы загружаются только тогда, когда они используются. То есть InnerClassSingletonHolder будет загружен при вызове метода getInstance(), а экземпляр SINGLETON в это время будет инициализирован, и он также гарантированно будет инициализирован только один раз. Этот метод не только обладает характеристиками безопасности потоков, но также имеет характеристики отложенной инициализации для экономии системных ресурсов. Метод испытания опущен.

Дальнейшее чтение:Сбор утром - загрузка статического внутреннего класса Java

перечисляемый класс

public enum EnumSingleton {
    INSTANCE;

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Применение одноэлементного шаблона

  • Logger Classes
  • Configuration Classes
  • Accesing resources in shared mode
  • Factories implemented as Singletons
  • java.lang.Runtime#getRuntime()
  • java.awt.Desktop#getDesktop()
  • java.lang.System#getSecurityManager()

Суммировать

  1. Шаги к рукописному одноэлементному шаблону
  • В классе есть собственная переменная (эта переменная может быть создана при ее использовании, а может быть создана до ее использования);
  • Убедитесь, что глобально существует только один экземпляр переменной;
  • Предоставьте общедоступный метод для доступа к переменной извне;
  1. Разница между различными режимами
различные реализации Функции
1. Ленивый - поток небезопасен Небезопасный поток, имеет характеристики ленивой загрузки для решения ресурсов;
2. Ленивый человек — безопасность потоков Модернизация 1 — добавление механизма синхронизации в глобальной точке доступа. Это может гарантировать безопасность потоков Хотя многопоточность может гарантировать согласованность, она не может гарантировать параллелизм.
3. Проверка двойной блокировки — потокобезопасность Измените 2, чтобы улучшить параллелизм, используйте volatile для изменения статических переменных экземпляра и проверьте, пусты ли переменные экземпляра до и после синхронизации. потокобезопасность
4. Hungry Man — безопасность потоков Модификация 1 создаст переменные экземпляра перед использованием. Глобальный объект будет создан только один раз, поэтому он может обеспечить безопасность потоков, но вызовет проблему пустой траты ресурсов.
5. Статический внутренний класс — потокобезопасность Преобразуйте 4 и разделите права на использование и права на создание переменных экземпляра, используя функцию, согласно которой статические внутренние классы загружаются только тогда, когда они используются. потокобезопасность
6. Класс перечисления — потокобезопасность Потокобезопасный, в основном подходит для сценариев с одним элементом

Дальнейшее чтение:одноэлементный шаблон