Подтвердить что ты не робот

Чем лучше модель синглтона Java сейчас?

Вы знаете, что, поскольку выпущена Java 5, рекомендуется использовать шаблон Singleton в Java, используя enum.

public enum Singleton {
    INSTANCE;
}

Но мне не нравится в этом - заставить клиента использовать Singleton.INSTANCE, чтобы иметь доступ к экземпляру singleton. Возможно, лучший способ скрыть Синглтон внутри обычного класса и обеспечить более лучший доступ к средствам однопользовательского режима:

public class ApplicationSingleton {
    private static enum Singleton {
        INSTANCE;               

        private ResourceBundle bundle;

        private Singleton() {
            System.out.println("Singleton instance is created: " + 
            System.currentTimeMillis());

            bundle = ResourceBundle.getBundle("application");
        }

        private ResourceBundle getResourceBundle() {
            return bundle;
        }

        private String getResourceAsString(String name) {
            return bundle.getString(name);
        }
    };

    private ApplicationSingleton() {}

    public static ResourceBundle getResourceBundle() {
        return Singleton.INSTANCE.getResourceBundle();
    }

    public static String getResourceAsString(String name) {
        return Singleton.INSTANCE.getResourceAsString(name);
    }
}

Итак, теперь клиент может просто написать:

ApplicationSingleton.getResourceAsString("application.name")

например. Что намного лучше:

Singleton.INSTANCE.getResourceAsString("application.name")

Итак, вопрос: правильно ли это сделать? Имеет ли этот код какие-либо проблемы (безопасность потока?)? Имеет ли он все преимущества, присущие шаблону "enum singleton"? Похоже, что он берет лучшее от мира. Как вы думаете? Есть ли лучший способ достичь этого? Благодарю.

ИЗМЕНИТЬ
@all
Прежде всего использование перечислений для шаблона Singleton было упомянуто в "Эффективной Java", 2-е издание: wikipedia: Java Enum Singleton. Я полностью согласен с тем, что мы должны максимально минимизировать использование Singleton, но мы не можем полностью уйти от них.
Прежде чем я приведу другой пример, позвольте мне сказать, что первый пример с ResourceBundle - это всего лишь случай, сам пример (и имена классов) не от реального приложения. Но, нужно сказать, что я не знал об управлении кешем ResourceBundle, спасибо за эту информацию)

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

Синглтон с использованием Enum:
Класс ApplicationSingleton:

public class ApplicationSingleton implements Serializable {
    private static enum Singleton {
        INSTANCE;               

        private Registry registry;

        private Singleton() {
            long currentTime = System.currentTimeMillis(); 
            System.out.println("Singleton instance is created: " + 
                    currentTime);

            registry = new Registry(currentTime);
        }

        private Registry getRegistry() {
            return registry;
        }

        private long getInitializedTime() {
            return registry.getInitializedTime();
        }

        private List<Registry.Data> getData() {
            return registry.getData();
        }
    };

    private ApplicationSingleton() {}

    public static Registry getRegistry() {
        return Singleton.INSTANCE.getRegistry();
    }

    public static long getInitializedTime() {
        return Singleton.INSTANCE.getInitializedTime();
    }

    public static List<Registry.Data> getData() {
        return Singleton.INSTANCE.getData();
    }    
}

Класс реестра:

public class Registry {
    private List<Data> data = new ArrayList<Data>();
    private long initializedTime;

    public Registry(long initializedTime) {
        this.initializedTime = initializedTime;
        data.add(new Data("hello"));
        data.add(new Data("world"));
    }

    public long getInitializedTime() {
        return initializedTime;
    }

    public List<Data> getData() {
        return data;
    }

    public class Data {      
        private String name;

        public Data(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }                   
    }
}

И тестовый класс:

public class ApplicationSingletonTest {     

    public static void main(String[] args) throws Exception {                   

        String rAddress1 = 
            ApplicationSingleton.getRegistry().toString();

        Constructor<ApplicationSingleton> c = 
            ApplicationSingleton.class.getDeclaredConstructor();
        c.setAccessible(true);
        ApplicationSingleton applSingleton1 = c.newInstance();
        String rAddress2 = applSingleton1.getRegistry().toString();

        ApplicationSingleton applSingleton2 = c.newInstance();
        String rAddress3 = applSingleton2.getRegistry().toString();             


        // serialization

        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(byteOut);
        out.writeObject(applSingleton1);

        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(byteOut.toByteArray()));
        ApplicationSingleton applSingleton3 = (ApplicationSingleton) in.readObject();

        String rAddress4 = applSingleton3.getRegistry().toString();

        List<Registry.Data> data = ApplicationSingleton.getData();
        List<Registry.Data> data1 = applSingleton1.getData();
        List<Registry.Data> data2 = applSingleton2.getData();
        List<Registry.Data> data3 = applSingleton3.getData();

        System.out.printf("applSingleton1=%s, applSingleton2=%s, applSingleton3=%s\n", applSingleton1, applSingleton2, applSingleton3);
        System.out.printf("rAddr1=%s, rAddr2=%s, rAddr3=%s, rAddr4=%s\n", rAddress1, rAddress2, rAddress3, rAddress4);
        System.out.printf("dAddr1=%s, dAddr2=%s, dAddr3=%s, dAddr4=%s\n", data, data1, data2, data3);
        System.out.printf("time0=%d, time1=%d, time2=%d, time3=%d\n",
                ApplicationSingleton.getInitializedTime(),
                applSingleton1.getInitializedTime(), 
                applSingleton2.getInitializedTime(),
                applSingleton3.getInitializedTime());
    }

}

И вот вывод:

Singleton instance is created: 1304067070250
[email protected], [email protected], [email protected]
[email protected], [email protected], [email protected], [email protected]
dAddr1=[[email protected], [email protected]], dAddr2=[[email protected], [email protected]], dAddr3=[[email protected], [email protected]], dAddr4=[[email protected], [email protected]]
time0=1304067070250, time1=1304067070250, time2=1304067070250, time3=1304067070250

Что нужно сказать:

  • Экземпляр Singleton был создан только один раз
  • Да, существует несколько разных экземпляров ApplicationSingletion, но все они содержат один и тот же экземпляр Singleton
  • Внутренние данные реестра одинаковы для всех разных экземпляров ApplicationSingleton

Итак, суммируем: подход Enum работает отлично и предотвращает дублирование создания Singleton путем отражения атаки и возвращает один и тот же экземпляр после сериализации.

Синглтон с использованием стандартного подхода:
Класс ApplicationSingleton:

public class ApplicationSingleton implements Serializable {
    private static ApplicationSingleton INSTANCE;

    private Registry registry;

    private ApplicationSingleton() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException ex) {}        
        long currentTime = System.currentTimeMillis();
        System.out.println("Singleton instance is created: " + 
                currentTime);
        registry = new Registry(currentTime);
    }

    public static ApplicationSingleton getInstance() {
        if (INSTANCE == null) {
            return newInstance();
        }
        return INSTANCE;

    }

    private synchronized static ApplicationSingleton newInstance() {
        if (INSTANCE != null) {
            return INSTANCE;
        }
        ApplicationSingleton instance = new ApplicationSingleton();
        INSTANCE = instance;

        return INSTANCE;
    }

    public Registry getRegistry() {
        return registry;
    }

    public long getInitializedTime() {
        return registry.getInitializedTime();
    }

    public List<Registry.Data> getData() {
        return registry.getData();
    }
}

Класс реестра (обратите внимание, что классы реестра и данных явно должны реализовывать Serializable для выполнения сериализации):

//now Registry should be Serializable in order serialization to work!!!
public class Registry implements Serializable {
    private List<Data> data = new ArrayList<Data>();
    private long initializedTime;

    public Registry(long initializedTime) {
        this.initializedTime = initializedTime;
        data.add(new Data("hello"));
        data.add(new Data("world"));
    }

    public long getInitializedTime() {
        return initializedTime;
    }

    public List<Data> getData() {
        return data;
    }

    // now Data should be Serializable in order serialization to work!!!
    public class Data implements Serializable {      
        private String name;

        public Data(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }                   
    }
}

И класс ApplicationSingletionTest (в основном тот же):

public class ApplicationSingletonTest {     

    public static void main(String[] args) throws Exception {

        String rAddress1 = 
            ApplicationSingleton.getInstance().getRegistry().toString();

        Constructor<ApplicationSingleton> c = 
            ApplicationSingleton.class.getDeclaredConstructor();
        c.setAccessible(true);
        ApplicationSingleton applSingleton1 = c.newInstance();
        String rAddress2 = applSingleton1.getRegistry().toString();

        ApplicationSingleton applSingleton2 = c.newInstance();
        String rAddress3 = applSingleton2.getRegistry().toString();             


        // serialization

        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(byteOut);
        out.writeObject(applSingleton1);

        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(byteOut.toByteArray()));
        ApplicationSingleton applSingleton3 = (ApplicationSingleton) in.readObject();

        String rAddress4 = applSingleton3.getRegistry().toString();

        List<Registry.Data> data = ApplicationSingleton.getInstance().getData();
        List<Registry.Data> data1 = applSingleton1.getData();
        List<Registry.Data> data2 = applSingleton2.getData();
        List<Registry.Data> data3 = applSingleton3.getData();

        System.out.printf("applSingleton1=%s, applSingleton2=%s, applSingleton3=%s\n", applSingleton1, applSingleton2, applSingleton3);
        System.out.printf("rAddr1=%s, rAddr2=%s, rAddr3=%s, rAddr4=%s\n", rAddress1, rAddress2, rAddress3, rAddress4);
        System.out.printf("dAddr1=%s, dAddr2=%s, dAddr3=%s, dAddr4=%s\n", data, data1, data2, data3);
        System.out.printf("time0=%d, time1=%d, time2=%d, time3=%d\n",
                ApplicationSingleton.getInstance().getInitializedTime(),
                applSingleton1.getInitializedTime(), 
                applSingleton2.getInitializedTime(),
                applSingleton3.getInitializedTime());
    }

}

И вот вывод:

Singleton instance is created: 1304068111203
Singleton instance is created: 1304068111218
Singleton instance is created: 1304068111234
[email protected], [email protected], [email protected]
[email protected], [email protected], [email protected], [email protected]
dAddr1=[[email protected], [email protected]], dAddr2=[[email protected], [email protected]], dAddr3=[[email protected], [email protected]], dAddr4=[[email protected], [email protected]]
time0=1304068111203, time1=1304068111218, time2=1304068111234, time3=1304068111218

Что нужно сказать:

  • Экземпляр Singleton был создан несколькими! раз
  • Все объекты реестра - это разные объекты со своими собственными данными.

Итак, подведем итог: стандартный подход является слабым для атаки отражения и возвращает другой экземпляр после сериализации, но да с теми же данными.


Итак, кажется, что подход Enum более прочный и надежный. И рекомендуется ли использовать шаблон Singleton в Java в наши дни? Как вы думаете?
Интересный факт для объяснения: почему объекты внутри enum могут быть сериализованы с его собственным классом, не реализует Serializable? Это функция или ошибка?

4b9b3361

Ответ 1

Я не знал, что перечисления были способом Java для создания синглов в наши дни. Но если вы собираетесь это сделать, вы можете просто использовать перечисление напрямую. Я не вижу веских оснований для инкапсуляции синглтона за кучей статических методов-членов; как только вы это сделаете, вы также можете написать статический класс с частными статическими членами.

Ответ 2

"Лучше" одноэлементный шаблон не должен использовать его.

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

Вместо этого используйте инъекцию зависимостей (с каркасом или без него, например Spring).

Ответ 3

Проблема, которую я вижу при таком подходе, - дублирование кода; если у вашего синглтона много методов, вы в конечном итоге пишете их дважды, чтобы убедиться, что ваша логика делегирования работает. Посмотрите на " инициализация по требованию владельца идиомы" для альтернативы вашему подходу, который является потокобезопасным и не нуждается в переименовании.

Ответ 4

[...] рекомендуемый способ записи Шаблон Singleton в Java использует enum [...]

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

Когда перечисление сериализовано, в его поток записывается только его имя, в основном потому, что ожидается, что характер перечисления полностью статичен. Когда enum десериализован, он строится снова на основе Enum.valueOf(name).

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

Это означает, что перечисления не всегда могут быть решением, хотя иногда они могут быть хорошим подходом.

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

Javadocs говорят:

Все загруженные пакеты ресурсов кэшируются по умолчанию.

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

ResourceBundle resource1 = ResourceBundle.getBundle("test");
ResourceBundle resource2 = ResourceBundle.getBundle("test");
assert resource1==resource2;

Если вы намерены сохранить некоторую память, вам не нужен механизм singleton. Предоставленный кеш может сделать трюк для вас.

Я не эксперт по этому вопросу, но если вы посмотрите на ResourceBundle Javadocs, возможно, вы сможете найти лучший способ справиться с ресурсным пакетом, отличным от этого enum singlenton.

Ответ 5

Мне нужно поблагодарить вас за этот разговор, но мне нужно обновить код частного конструктора как:

private ApplicationSingleton() {
    long currentTime = System.currentTimeMillis();
    System.out.println("Singleton instance is created: " + currentTime);
}

И вот вывод:

Singleton instance is created: 1347981459285
Singleton instance is created: 1347981459285
Singleton instance is created: 1347981459285
[email protected],        
[email protected],   
[email protected]

Что нужно сказать:

  • Экземпляр Singleton был создан несколькими! раз
  • Все объекты реестра - это разные объекты со своими собственными данными.

потому что мы вынуждаем частный конструктор быть открытым в c.setAccessible(истина);

Значение true указывает, что отраженный объект должен подавлять проверку доступа к языку Java, когда он используется. Значение false указывает, что отраженный объект должен обеспечивать проверку доступа к языку Java.

поэтому для проверки безопасности потоков singleton вам необходимо использовать многопоточное приложение

Ответ 6

Подход enum для синглтонов был популяризирован Джошуа Блохом в его книге Эффективная Java. Другим хорошим способом является ленивый шаблон владельца, который похож на идею OP. Я думаю, что скрытие enum в классе, как предлагает OP, не добавит никакой производительности или concurrency рисков.

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

Ответ 7

Мне нравится перечисление для Singleton, но шоу-стоппер - это когда вам нужно наследование (как я здесь). Перечисления не могут наследовать. С dp4j минимальный синглтон выглядит следующим образом:

@com.dp4j.Singleton //(lazy=false)
public class MySingleton extends Parent{}

dp4j действительно создаст это:

@Singleton
public class ApplicationSingleton extends Parent{

  @instance
  private static ApplicationSingleton instance = new ApplicationSingleton();

  private ApplicationSingleton(){}

  @getInstance
  public static ApplicationSingleton getInstance(){
     return instance;
  }

Как вы отмечаете, это решение уязвимо для атаки "Отражение". На dp4j.com действительно есть демонстрация того, как unit test Singleton использует API Reflection.