Паттерн синглтон, ты правда правильно пишешь?

Java

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