Статьи

Осуществление Java Singletons очень сложно

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

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

Окончательная полевая схема

Это решение опирается на то, что конструктор остается закрытым и экспортируется открытый статический конечный элемент, чтобы обеспечить одноэлементный доступ, например так:

public class FooSingleton {

    public final static FooSingleton INSTANCE = new FooSingleton();

    private FooSingleton() { }

    public void bar() { }

}

При первом обращении к классу инициализируются статические члены, в результате чего закрытый конструктор вызывается только один раз. Гарантируется, что, хотя несколько потоков могут ссылаться на класс до его инициализации, JVM обеспечит правильную инициализацию класса, прежде чем потоки смогут продолжить работу. Однако существует риск, что кто-то может создать поддельный экземпляр класса, используя отражение, используя метод setAccessible (true). Вот как это можно сделать:

Constructor[] constructors = FooSingleton.class.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
FooSingleton spuriousFoo = (FooSingleton) constructor.newInstance(new Object[0]);

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

public class FooSingleton2 {

    private static boolean INSTANCE_CREATED;
    public final static FooSingleton2 INSTANCE = new FooSingleton2();

    private FooSingleton2() {
        if (INSTANCE_CREATED) {
            throw new IllegalStateException("You must only create one instance of this class");
        } else {
            INSTANCE_CREATED = true;
        }
    }

    public void bar() { }

}

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

Field f = FooSingleton2.class.getDeclaredField("INSTANCE_CREATED");
f.setAccessible(true);
f.set(null, false);

Constructor[] constructors = FooSingleton2.class.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
FooSingleton2 spuriousFoo = (FooSingleton2) constructor.newInstance(new Object[0]);

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

Схема статической фабрики

В этом подходе открытый член является статической фабрикой, подобной этой:

public class FooSingleton3 {

    public final static FooSingleton3 INSTANCE = new FooSingleton3();

    private FooSingleton3() { }

    public static FooSingleton3 getInstance() { return INSTANCE; }

    public void bar() { }

}

Когда вызывается getInstance (), он всегда возвращает одну и ту же ссылку на объект. Хотя эта схема имеет ту же уязвимость отражения, что и предыдущая, она имеет некоторые преимущества. Например, вы можете изменить применение синглтона, не изменяя фактический API, как вы увидите позже в этом посте. Исторически метод getInstance () использовался для большинства одноэлементных реализаций, и, по фактическим соглашениям, он представляет собой явный маркер того, что это действительно одноэлементный объект .

Идиома держателя инициализации по требованию

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

public class FooSingleton4 {

    private FooSingleton4() {
    }

    public static FooSingleton4 getInstance() {
        return FooSingleton4Holder.INSTANCE;
    }

    private static class FooSingleton4Holder {

        private static final FooSingleton4 INSTANCE = new FooSingleton4();
    }
}

Ищите Сериализуемый

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

Enum Идиома

Эта схема просто использует Enum в качестве держателя одного члена INSTANCE, например:

public enum FooEnumSingleton {

    INSTANCE;

    public static FooEnumSingleton getInstance() { return INSTANCE; }

    public void bar() { }

}

В соответствии со спецификацией языка Java §8.9 «Последний метод клонирования в Enum гарантирует, что константы enum никогда не могут быть клонированы, а специальная обработка механизмом сериализации гарантирует, что повторяющиеся экземпляры никогда не будут созданы в результате десериализации. Рефлексивная реализация типов перечислений запрещено. Вместе эти четыре вещи гарантируют, что экземпляров типа enum не существует, кроме тех, которые определены константами enum. «

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

 Constructor con = FooEnumSingleton.class.getDeclaredConstructors()[0];
 Method[] methods = con.getClass().getDeclaredMethods();
 for (Method method : methods) {
     if (method.getName().equals("acquireConstructorAccessor")) {
         method.setAccessible(true);
         method.invoke(con, new Object[0]);
     }
  }
  Field[] fields = con.getClass().getDeclaredFields();
  Object ca = null;
  for (Field field : fields) {
      if (field.getName().equals("constructorAccessor")) {
          field.setAccessible(true);
          ca = field.get(con);
      }
  }
  Method method = ca.getClass().getMethod("newInstance", new Class[]{Object[].class});
  method.setAccessible(true);
  FooEnumSingleton spuriousEnum = (FooEnumSingleton) method.invoke(ca, new Object[]{new Object[]{"SPURIOUS_INSTANCE", 1}});
  printInfo(FooEnumSingleton.INSTANCE);
  printInfo(spuriousEnum);
}

private static void printInfo(FooEnumSingleton e) {
    System.out.println(e.getClass() + ":" + e.name() + ":" + e.ordinal());
}

Когда мы запускаем код, мы получаем следующий вывод:

class com.blogspot.minborgsjavapot.singleton.FooEnumSingleton:INSTANCE:0
class com.blogspot.minborgsjavapot.singleton.FooEnumSingleton:SPURIOUS_INSTANCE:1

Недостатком схемы Enum является то, что мы не можем наследовать от другого базового класса, потому что перечисления не могут расширять другой класс. Причина в том, что он уже по своей сути расширяет java.lang.Enum. Если вы хотите имитировать наследование, вы можете рассмотреть шаблон интерфейса, как описано в моем предыдущем посте здесь .

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

Выводы

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

Enum предоставляет хороший и простой инструмент для одиноких.  Initialization по требованию держателя идиома обеспечивает хороший образец , если вы хотите использовать наследование и / или хотите ленивую инициализацию .

Удачи вам с вашими одиночками!