Интервьюер: Пожалуйста, напишите синглтон, который вы считаете «идеальным».

Java

Одноэлементный шаблон гарантирует наличие одного и только одного экземпляра класса, что больше подходит для сценариев, требующих управления ресурсами (например, пулами соединений с базой данных) или совместным использованием ресурсов (например, классы инструментов с отслеживанием состояния). Если мы напишем реализацию синглтона, по оценкам, большинство людей думают, что они хороши, но если вам нужно реализовать относительно совершенный синглтон, это может быть не так просто, как вы думаете. В этой статье в качестве основы взято интервью главного героя Сяоюй, и шаг за шагом обсуждается, как реализовать более «идеальный» синглтон. Персонажи и сцены в этой статье вымышлены, а любые сходства сфабрикованы.

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

Простая реализация синглтона

С пониманием и впечатлением от синглтона Сяоюй написал следующий код

public class Singleton {
    private static Singleton instance;

    private Singleton(){}

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

После написания Сяоюй снова просмотрел его и всегда чувствовал, что он был слишком простым, и казалось, что он далек от «идеального». Да, в многопоточной параллельной среде эта реализация не может работать.Если два потока вызывают метод getInstance() одновременно и одновременно выполняют суждение if, обе стороны думают, что экземпляр экземпляра пуст, и объект Singleton будет создан, что приведет как минимум к двум экземплярам, ​​подумал Сяоюй. Что ж, нам нужно решить проблему синхронизации в многопоточной параллельной среде, чтобы обеспечить потокобезопасность синглтонов.

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

Когда дело доходит до параллельной синхронизации, Сяоюй думает о блокировках. Не так просто добавить блокировку, синхронизировать ее,

public class Singleton {
    private static Singleton instance;

    private Singleton(){}

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

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

Подумав и вспомнив некоторое время, Сяоюй вспомнил, что видел штуку под названием Блокировка с двойной проверкой, блокировка с двойной проверкой, хм, давайте ее оптимизируем,

public class Singleton {
    private static volatile Singleton instance;

    private Singleton(){}

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

После того, как синглтон создан в первый раз, последующие вызовы getInstance() сначала оцениваются как нулевые, и если он не пуст, он возвращается напрямую.Если он пуст, даже если два потока определены как пустые в то же время в блоке синхронизации также выполняется дублирование.Проверьте, что он будет создан только один раз, что устраняет ненужные накладные расходы на блокировку и обеспечивает потокобезопасность. И что заставляет Сяою чувствовать себя удовлетворенным, так это то, что он добавил ключевое слово volatile на основе некоторого понимания JVM, чтобы избежать проблем, которые могут быть вызваны оптимизацией переупорядочения инструкций во время создания экземпляра, что на самом деле является последним штрихом. Просто - идеально!

Советы: семантика ключевого слова volatile

  1. Гарантированная видимость переменных для всех потоков. При записи значения в переменную JMM (модель памяти Java) обновит значение рабочей памяти текущего потока в основную память.При чтении JMM будет считывать значение переменной из основной памяти вместо рабочей памяти в обеспечить переменное значение.После обновления одним потоком другой поток может немедленно прочитать обновленное значение.
  2. Отключите оптимизацию переупорядочения инструкций. Чтобы повысить производительность, когда JVM выполняет программы, компиляторы и процессоры часто меняют порядок инструкций.Использование volatile может отключить оптимизацию переупорядочения инструкций.

Когда JVM создает новый экземпляр, он в основном выполняет три шага:

  1. Выделить память
  2. инициализатор
  3. Укажите ссылку на объект на выделенный адрес памяти

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

Сяоюй передал ответ интервьюеру. Интервьюер взглянул на него и сказал: «Это в принципе можно использовать, но если я использую отражение для прямого вызова конструктора этого класса, невозможно ли гарантировать одноэлементность?» Сяоюй почесал затылок, да , о, если вы используете отражение, вы можете изменить видимость конструктора синглтона во время выполнения и вызвать конструктор напрямую для создания нового экземпляра, например, с помощью следующего кода

 Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
 constructor.setAccessible(true);
 Singleton singleton = constructor.newInstance();

Сяоюй снова задумался.

безопасный для отражения синглтон

Как предотвратить отражение от уничтожения синглтона?Может быть, вы можете добавить статическую переменную для управления им, чтобы конструктор был действителен только в том случае, если он вызывается изнутри getInstance(), и будет выброшено исключение, если он не вызывается напрямую через getInstance(). Xiaoyu сделал что-то в соответствии с этой идеей.

public class Singleton {
    private static volatile Singleton instance;
    private static boolean flag = false;

    private Singleton(){
        synchronized (Singleton.class) {
            if (flag) {
                flag = false;
            } else {
                throw new RuntimeException("Please use getInstance() method to get the single instance.");
            }
        }

    }

    public static final Singleton getInstance(){
        if(instance == null) {
            synchronized (Singleton.class){
                if(instance == null) {
                    flag = true;
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Флаг статической переменной используется для управления, и конструктор может быть создан нормально только вызовом конструктора из getInstance(), в противном случае будет выдано исключение. Но Xiaoyu сразу обнаружил проблему: поскольку конструктор можно вызвать через отражение, значение флага тоже можно изменить через отражение, поэтому кропотливо выстроенная логика управления флагом будет нарушена. Он тоже не кажется таким уж "идеальным". Хотя он не так совершенен, он также в определенной степени избегает сцены прямого вызова конструктора с использованием отражения, и кажется, что нет лучшего способа придумать это, поэтому Сяоюй представил ответ.

Интервьюер показал зачарованную улыбку: «Идея очень хорошая, проблема отражения в основном решена, но если я сериализую этот одноэлементный объект, а затем десериализую его в объект, останутся ли эти два объекта одинаковыми, и могу ли я по-прежнему гарантировать одноэлементный объект? Например. Если нет, то как решить эту проблему?"

SerializationSafeSingleton s1 = SerializationSafeSingleton.getInstance();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(s1);
oos.close();

ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
SerializationSafeSingleton s2 = (SerializationSafeSingleton) ois.readObject();
ois.close();

s1 == s2? Ответ нет и как это решить.

Синглтон, безопасный для сериализации

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

public class Singleton {
    private static volatile Singleton instance;
    private static boolean flag = false;

    private Singleton(){
        synchronized (Singleton.class) {
            if (flag) {
                flag = false;
            } else {
                throw new RuntimeException("Please use getInstance() method to get the single instance.");
            }
        }

    }

    public static final Singleton getInstance(){
        if(instance == null) {
            synchronized (Singleton.class){
                if(instance == null) {
                    flag = true;
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    /**
     * 该方法代替了从流中读取对象
     * @return
     */
    private Object readResolve(){
        return getInstance();
    }
}

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

Другие варианты

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

1. Статическая версия инициализации

public class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton(){}

    public static final Singleton getInstance() {
        return instance;
    }
}

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

2. Версия статического внутреннего класса

public class Singleton {
    private Singleton(){}

    public static final Singleton getInstance() {
        return SingletonHolder.instance;
    }

    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }
}

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

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

«Идеальное» решение

После долгих размышлений Сяоюй внезапно озарился вспышкой света, перечисляя! (Это также способ, рекомендованный автором «Эффективной Java»)

public enum Singleton {
    INSTANCE;

    public void func(){
        ...
    }
}

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

Советы. Почему поток перечисления, отражение и сериализация безопасны?

  1. Перечисление фактически реализуется конечным классом, унаследованным от Enum (конкретную реализацию можно увидеть, декомпилировав файл класса), а его члены инициализируются в статическом блоке кода, поэтому механизм загрузки класса используется для обеспечения его потокобезопасности.
  2. Перечисление не поддерживает создание экземпляров посредством отражения, что можно увидеть в методе newInstance класса Constructor.
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
     throw new IllegalArgumentException("Cannot reflectively create enum objects");
  3. Когда перечисление сериализовано, оно выводит в результат только атрибут имени объекта перечисления, а при десериализации используется метод valueOf класса java.lang.Enum для поиска объекта перечисления по имени. Кроме того, компилятор не позволяет настраивать этот механизм сериализации, отключая такие методы, как writeObject, readObject, readObjectNoData, writeReplace и readResolve. Благодаря этому механизму перечисление гарантирует безопасность сериализации.

Суммировать

Схема перечисления практически «идеальна», но на практике в большинстве случаев мы используем схему блокировки с двойной проверкой или схему статического внутреннего класса, которая в принципе достаточна для нашего сценария и хорошо работает. А решения никогда не бывают «идеальными», они бывают только лучшими или более подходящими. Эта статья предназначена только для того, чтобы понять или просмотреть знания об отражении, сериализации, безопасности потоков, модели памяти Java (изменчивая семантика), механизме загрузки классов JVM, оптимизации переупорядочения инструкций JVM и т. д. из непрерывной эволюции одноэлементной реализации. больше думать с разных сторон и максимально комплексно рассматривать проблему в процессе проектирования или реализации. Или лучше удовлетворить «идеальные» ожидания интервьюера в соответствующем интервью.


Автор: Юге, ветеран ИТ, который все еще учится. Добро пожаловать, чтобы обратить внимание на официальный аккаунт автора: песня Halfway Rain, учитесь и растите вместе
qrcode