Интервьюер был ошеломлен, когда прочитал мой рукописный синглтон!

Java

предисловие

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

Ранее я представил несколько распространенных способов написания одноэлементного шаблона. Если вы еще этого не знаете, загляните на портал здесь:

одноэлементный шаблон проектирования

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

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

Одноэлементный шаблон DCL

ДКЛ этоDouble Check LАббревиатура от ock, блокировка синхронизации с двойной проверкой. код показывает, как показано ниже,

public class Singleton {

    //注意,此变量需要用volatile修饰以防止指令重排序
    private static volatile Singleton singleton = null;

    private Singleton(){

    }

    public static Singleton getInstance(){
        //进入方法内,先判断实例是否为空,以确定是否需要进入同步代码块
        if(singleton == null){
            synchronized (Singleton.class){
                //进入同步代码块时再次判断实例是否为空
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

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

Но тут возникает проблема.

Гарантируется ли одноэлементность DCL потокобезопасности?

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

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

Однако мы знаем, что в Java есть очень мощная функция —отражение. Да, верно, это он.

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

public class TestDCL {
    public static void main(String[] args) throws Exception {
        Singleton singleton1 = Singleton.getInstance();
        System.out.println(singleton1.hashCode()); // 723074861
        Class<Singleton> clazz = Singleton.class;
        Constructor<Singleton> ctr = clazz.getDeclaredConstructor();
        //通过反射拿到无参构造,设为可访问
        ctr.setAccessible(true);
        Singleton singleton2 = ctr.newInstance();
        System.out.println(singleton2.hashCode()); // 895328852
    }
}

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

Напечатанный hashCode отличается, что указывает на то, что это два разных объекта.

Так как же предотвратить отражение от уничтожения синглетонов?

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

Измените конструктор следующим образом:

Повторный запуск тестового кода вызовет исключение.

Эффективно предотвращает создание объектов посредством отражения.

Итак, можно ли написать такой синглтон?

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

Но в чем проблема?

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

Практика приносит истинное знание, и мы узнаем его, проверяя.

// 给 Singleton 添加序列化的标志,表明可以序列化
public class Singleton implements Serializable{ 
    ... //省略不重要代码
}
//测试是否返回同一个对象
public class TestDCL {
    public static void main(String[] args) throws Exception {
        Singleton singleton1 = Singleton.getInstance();
        System.out.println(singleton1.hashCode()); // 723074861
        //通过序列化对象,再反序列化得到新对象
        String filePath = "D:\\singleton.txt";
        saveToFile(singleton1,filePath);
        Singleton singleton2 = getFromFile(filePath);
        System.out.println(singleton2.hashCode()); // 1259475182
    }

    //将对象写入到文件
    private static void saveToFile(Singleton singleton, String fileName){
        try {
            FileOutputStream fos = new FileOutputStream(fileName);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(singleton); //将对象写入oos
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //从文件中读取对象
    private static Singleton getFromFile(String fileName){
        try {
            FileInputStream fis = new FileInputStream(fileName);
            ObjectInputStream ois = new ObjectInputStream(fis);
            return (Singleton) ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}

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

Итак, как решить эту проблему?

Сначала я расскажу о решении, а затем объясню, почему оно работает.

Это очень просто, просто добавьте метод readResolve в класс singleton и позвольте ему вернуть объект singleton, который мы создали в теле метода.

Затем снова запустите тестовый класс, и вы обнаружите, что напечатанный hashCode тот же.

Разве это не удивительно. . .

Почему readResolve решает проблему сериализации, уничтожающей синглтоны?

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

Давайте подумаем, какой процесс, скорее всего, имеет место для манипуляций в процессе сериализации и десериализации.

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

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

Это должно быть возможно. Итак, подходя к методу, который мы написалиgetFromFile, найти эту строкуois.readObject(). Это метод, который читает объект из потока.

Нажмите, чтобы просмотретьObjectInputStream.readObject 方法, затем найдитеreadObject0()方法

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

а затем увидеть, что естьreadOrdinaryObject方法, нажмите.

Затем найдите эту строку,isInstantiable()метод, чтобы определить, можно ли создать экземпляр объекта.

Поскольку конструктор cons не пуст, этот метод возвращает значение true. Таким образом создается непустой объект obj.

Иди дальше вниз, звони,hasReadResolveMethodметод определения переменнойreadResolveMethodЯвляется ли он ненулевым.

Давайте посмотрим на эту переменную, где она назначена или нет. Вы найдете такой фрагмент кода,

нажмите этот методgetInheritableMethod. Обнаружил, что наконец-то нужно вернуть то, что мы добавилиreadResolveметод.

При этом мы обнаружили, что модификатор этого метода может быть public, protected или private (мы сейчас используем private). Однако статические и абстрактные украшения не допускаются.

назад сноваreadOrdinaryObjectметод, продолжайте спускаться, вы обнаружите, что вызов сделанinvokeReadResolveметод. Этот метод вызывается через отражениеreadResolveметод, получил объект rep.

Затем проверьте, равен ли rep объекту. obj — это новый объект, который мы только что создали с помощью конструктора, и поскольку мы переписали метод readResolve, чтобы напрямую возвращать объект-одиночку, rep — это исходный объект-одиночка, который не равен obj.

Итак, назначьте rep объекту и верните объект.

Таким образом, мы, наконец, получаем этот объект obj, который является нашим исходным объектом-одиночкой.

В этот момент мы понимаем, что происходит.

Подводя итог в одном предложении: когда объект считывается из потока объектов ObjectInputStream, проверяется, определяет ли класс объекта метод readResolve. Если он определен, вызовите его, чтобы вернуть объект, который мы хотим указать (здесь указывается возвращаемый одноэлементный объект).

Суммировать

Следовательно, полный DCL можно записать так:

public class Singleton implements Serializable {

    //注意,此变量需要用volatile修饰以防止指令重排序
    private static volatile Singleton singleton = null;

    private Singleton(){
        if(singleton != null){
            throw new RuntimeException("Can not do this");
        }
    }

    public static Singleton getInstance(){
        //进入方法内,先判断实例是否为空,以确定是否需要进入同步代码块
        if(singleton == null){
            synchronized (Singleton.class){
                //进入同步代码块时再次判断实例是否为空
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    // 定义readResolve方法,防止反序列化返回不同的对象
    private Object readResolve(){
        return singleton;
    }
}

Кроме того, я не знаю, заметили ли внимательные читатели наличие в исходном коде ветки switch.case TC_ENUMфилиал. Вот обработка типа перечисления.

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

Ищите "Misty Rain Starry Sky" на WeChat, еще больше хороших статей бесплатно~