Комикс: Что такое одноэлементный шаблон?

Java JVM переводчик Безопасность






----- На следующий день -----















Первая версия одноэлементного паттерна:


public class Singleton {
    private Singleton() {}  //私有构造函数
    private static Singleton instance = null;  //单例对象
    //静态工厂方法
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}


Почему ты это пишешь? Давайте объясним несколько ключевых моментов:


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


2.instance — это статический член класса Singleton, который также является нашим объектом singleton. Его начальное значение может быть записано как Null или как new Singleton(). Разница будет объяснена позже.


3.getInstance — это метод получения одноэлементного объекта.


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


Если одноэлементный объект активно создается с помощью new Singleton() в начале, нулевая операция больше не требуется.Этот метод записи принадлежитрежим голодного человека.


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







Почему только что код не является потокобезопасным?


Предположим, что класс Singleton только что инициализировался, а объект-экземпляр еще пуст, и в это время два потока одновременно обращаются к методу getInstance:




Поскольку экземпляр пуст, два потока одновременно принимают условное решение и начинают выполнять новую операцию:





В результате экземпляр явно создается дважды. Вносим изменения в код:



Синглтон Узор Второе издание:


public class Singleton {
    private Singleton() {}  //私有构造函数
   private static Singleton instance = null;  //单例对象
   //静态工厂方法
   public static Singleton getInstance() {
        if (instance == null) {      //双重检测机制
         synchronized (Singleton.class){  //同步锁
           if (instance == null) {     //双重检测机制
             instance = new Singleton();
               }
            }
         }
        return instance;
    }
}


Почему так написано? Поясним несколько ключевых моментов:


1. Чтобы предотвратить многократное выполнение нового синглтона, перед новой операцией добавляется блокировка синхронизации Synchronized для блокировки всего класса (обратите внимание, что здесь нельзя использовать блокировки объектов).


2. После входа в Синхронизированную критическую секцию необходимо сделать еще одно пустое суждение. Потому что, когда два потока обращаются к объекту одновременно, поток A завершил построение объекта, а поток B также прошел первоначальную нулевую оценку.













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













————————————











Предположим такой сценарий, когда два потока один за другим обращаются к методу getInstance, когда поток A строит объект, поток B только что вошел в метод:




Кажется, в этой ситуации нет проблем: либо Экземпляр не был создан потоком A, и поток B получает значение true при выполнении if (instance == null); либо Экземпляр был создан потоком A, а поток B выполняется if ( instance == null), чтобы получить false.


Это действительно так? ответ отрицательный. Это включает в себя компилятор JVMперестановка инструкций.


Что означает изменение порядка инструкций? Например, простое предложение instance = new Singleton в java будет скомпилировано компилятором в следующие инструкции JVM:


Memory = allocate(); // 1: Место в памяти для размещения объектов

ctorInstance(memory); //2: Инициализировать объект

instance =memory; //3: Установите instance так, чтобы он указывал на только что выделенный адрес памяти


Однако порядок этих инструкций не является статическим и может быть оптимизирован JVM и ЦП, а инструкции переупорядочены в следующем порядке:


memory =allocate(); //1: выделить место в памяти объекта

instance =memory; //3: Установите instance так, чтобы он указывал на только что выделенный адрес памяти

ctorInstance(memory); //2: Инициализировать объект


Когда поток A завершает выполнение 1, 3, объект экземпляра не был инициализирован, но больше не указывает на null. В это время, если поток B вытесняет ресурсы ЦП, результат выполнения if (instance == null) будет ложным, таким образом возвращаяОбъект INSTANCE без инициализации. Как показано ниже:







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



Третье издание шаблона Singleton:


public class Singleton {
    private Singleton() {}  //私有构造函数
    private volatile static Singleton instance = null;  //单例对象
    //静态工厂方法
    public static Singleton getInstance() {
          if (instance == null) {      //双重检测机制
         synchronized (Singleton.class){  //同步锁
           if (instance == null) {     //双重检测机制
             instance = new Singleton();
                }
             }
          }
          return instance;
      }
}







The volatile keyword indicates that a value may change between different accesses, it prevents an optimizing compiler from optimizing away subsequent reads or writes and thus incorrectly reusing a stale value or omitting writes.







После изменения volatile, когда поток A выполняет instance = new Singleton, каков порядок выполнения JVM? Всегда гарантируется следующий порядок:


memory =allocate(); //1: выделить место в памяти объекта

ctorInstance(memory); //2: Инициализировать объект

instance =memory; //3: Установите instance так, чтобы он указывал на только что выделенный адрес памяти


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









Реализуйте шаблон singleton со статическим внутренним классом:


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


Вот несколько замечаний:


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


2. Момент инициализации объекта INSTANCE определяется не моментом загрузки одноэлементного класса Singleton, а моментом вызова метода getInstance для загрузки статического внутреннего класса LazyHolder. Итак, эта реализация используетМеханизм загрузки classloaderЧтобы реализовать ленивую нагрузку и обеспечить безопасность потоков здания синглтонов.











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



Используйте отражение, чтобы сломать синглтон:

//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));



Код можно упростить до трех шагов:


Первый шаг — получить конструктор одноэлементного класса.


Второй шаг — сделать конструктор доступным.


Третий шаг — создать объект с помощью метода newInstance.


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







Реализуйте одноэлементный шаблон с помощью перечисления:


public enum SingletonEnum {
    INSTANCE;
}






Давайте проведем эксперимент, все еще выполняя код отражения прямо сейчас:


//获得构造器
Constructor con = SingletonEnum.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
SingletonEnum singleton1 = (SingletonEnum)con.newInstance();
SingletonEnum singleton2 = (SingletonEnum)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));



При выполнении шага получения конструктора выбрасывается следующее исключение:


Exception in thread "main" java.lang.NoSuchMethodException: com.xiaohui.singleton.test.SingletonEnum.<init>()

at java.lang.Class.getConstructor0(Class.java:2892)

at java.lang.Class.getDeclaredConstructor(Class.java:2058)

at com.xiaohui.singleton.test.SingletonTest.main(SingletonTest.java:22)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

at java.lang.reflect.Method.invoke(Method.java:606)

at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)













Несколько дополнений:


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


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


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


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




----КОНЕЦ----




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