Вопрос-ответ

What is an efficient way to implement a singleton pattern in Java? [closed]

Какой эффективный способ реализовать одноэлементный шаблон в Java?

Каков эффективный способ реализовать одноэлементный шаблон проектирования в Java?

Переведено автоматически
Ответ 1

Используйте перечисление:

public enum Foo {
INSTANCE;
}

Джошуа Блох объяснил этот подход в своем выступлении "Эффективная перезагрузка Java" на Google I / O 2008: ссылка на видео. Также смотрите слайды 30-32 его презентации (effective_java_reloaded.pdf):


Правильный способ реализовать сериализуемый одноэлементный шаблон


public enum Elvis {
INSTANCE;
private final String[] favoriteSongs =
{ "Hound Dog", "Heartbreak Hotel" };
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
}

Редактировать: В онлайн-части "Эффективной Java" говорится:


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


Ответ 2

В зависимости от использования существует несколько "правильных" ответов.

Начиная с Java 5, лучший способ сделать это - использовать enum:

public enum Foo {
INSTANCE;
}

До Java 5 самым простым случаем было:

public final class Foo {

private static final Foo INSTANCE = new Foo();

private Foo() {
if (INSTANCE != null) {
throw new IllegalStateException("Already instantiated");
}
}

public static Foo getInstance() {
return INSTANCE;
}

public Object clone() throws CloneNotSupportedException{
throw new CloneNotSupportedException("Cannot clone instance of this class");
}
}

Давайте рассмотрим код. Во-первых, вы хотите, чтобы класс был final . В этом случае я использовал final ключевое слово, чтобы пользователи знали, что оно final . Тогда вам нужно сделать конструктор закрытым, чтобы пользователи не могли создавать свои собственные Foo . Выдача исключения из конструктора не позволяет пользователям использовать отражение для создания второго Foo. Затем вы создаете private static final Foo поле для хранения единственного экземпляра и public static Foo getInstance() метод для его возврата. Спецификация Java гарантирует, что конструктор вызывается только при первом использовании класса.

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

Вы можете использовать private static class для загрузки экземпляра. Тогда код будет выглядеть следующим образом:

public final class Foo {

private static class FooLoader {
private static final Foo INSTANCE = new Foo();
}

private Foo() {
if (FooLoader.INSTANCE != null) {
throw new IllegalStateException("Already instantiated");
}
}

public static Foo getInstance() {
return FooLoader.INSTANCE;
}
}

Поскольку строка private static final Foo INSTANCE = new Foo(); выполняется только тогда, когда фактически используется класс FooLoader, это устраняет отложенное создание экземпляра и гарантирует потокобезопасность.

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

public final class Foo implements Serializable {

private static final long serialVersionUID = 1L;

private static class FooLoader {
private static final Foo INSTANCE = new Foo();
}

private Foo() {
if (FooLoader.INSTANCE != null) {
throw new IllegalStateException("Already instantiated");
}
}

public static Foo getInstance() {
return FooLoader.INSTANCE;
}

@SuppressWarnings("unused")
private Foo readResolve() {
return FooLoader.INSTANCE;
}
}

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

Ответ 3

Отказ от ответственности: я только что обобщил все потрясающие ответы и написал их своими словами.


При реализации одноэлементного шаблона у нас есть два варианта:


  1. Отложенная загрузка

  2. Ранняя загрузка

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

Самый простой способ реализации одноэлементного шаблона - это:

public class Foo {

// It will be our sole hero
private static final Foo INSTANCE = new Foo();

private Foo() {
if (INSTANCE != null) {
// SHOUT
throw new IllegalStateException("Already instantiated");
}
}

public static Foo getInstance() {
return INSTANCE;
}
}

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

class Foo {

// Our now_null_but_going_to_be sole hero
private static Foo INSTANCE = null;

private Foo() {
if (INSTANCE != null) {
// SHOUT
throw new IllegalStateException("Already instantiated");
}
}

public static Foo getInstance() {
// Creating only when required.
if (INSTANCE == null) {
INSTANCE = new Foo();
}
return INSTANCE;
}
}

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

class Foo {

private static Foo INSTANCE = null;

// TODO Add private shouting constructor

public static Foo getInstance() {
// No more tension of threads
synchronized (Foo.class) {
if (INSTANCE == null) {
INSTANCE = new Foo();
}
}
return INSTANCE;
}
}

Но этого недостаточно, чтобы защитить нашего героя, на самом деле!!! Это лучшее, что мы можем / должны сделать, чтобы помочь нашему герою:

class Foo {

// Pay attention to volatile
private static volatile Foo INSTANCE = null;

// TODO Add private shouting constructor

public static Foo getInstance() {
if (INSTANCE == null) { // Check 1
synchronized (Foo.class) {
if (INSTANCE == null) { // Check 2
INSTANCE = new Foo();
}
}
}
return INSTANCE;
}
}

Это называется "идиома блокировки с двойной проверкой". Легко забыть оператор volatile и трудно понять, зачем это необходимо.
Подробнее: Объявление "Дважды проверенная блокировка нарушена"

Теперь мы уверены в злых потоках, но как насчет жестокой сериализации? Мы должны убедиться, что даже во время десериализации не создается новый объект:

class Foo implements Serializable {

private static final long serialVersionUID = 1L;

private static volatile Foo INSTANCE = null;

// The rest of the things are same as above

// No more fear of serialization
@SuppressWarnings("unused")
private Object readResolve() {
return INSTANCE;
}
}

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

Наконец, мы добавили достаточную защиту от потоков и сериализации, но наш код выглядит громоздким и уродливым. Давайте переделаем нашего героя.:

public final class Foo implements Serializable {

private static final long serialVersionUID = 1L;

// Wrapped in a inner static class so that loaded only when required
private static class FooLoader {

// And no more fear of threads
private static final Foo INSTANCE = new Foo();
}

// TODO add private shouting construcor

public static Foo getInstance() {
return FooLoader.INSTANCE;
}

// Damn you serialization
@SuppressWarnings("unused")
private Foo readResolve() {
return FooLoader.INSTANCE;
}
}

Да, это тот самый наш герой :)

Поскольку строка private static final Foo INSTANCE = new Foo(); выполняется только тогда, когда фактически используется класс FooLoader, это устраняет отложенное создание экземпляра и гарантирует потокобезопасность.

И мы зашли так далеко. Вот лучший способ добиться всего, что мы сделали, - это наилучший из возможных способов:

public enum Foo {
INSTANCE;
}

Который внутренне будет обрабатываться как

public class Foo {

// It will be our sole hero
private static final Foo INSTANCE = new Foo();
}

Вот и все! Больше не нужно бояться сериализации, потоков и уродливого кода. Также ПЕРЕЧИСЛЕНИЯ singleton лениво инициализируются.


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


-Джошуа Блох в книге "Эффективная Java"

Теперь вы, возможно, поняли, почему ПЕРЕЧИСЛЕНИЯ считаются лучшим способом реализации одноэлементного шаблона, и спасибо за ваше терпение :)

Обновил его в моем блоге.

Ответ 4

Решение, опубликованное Стю Томпсоном, действительно в Java 5.0 и более поздних версиях. Но я бы предпочел не использовать его, потому что считаю, что оно подвержено ошибкам.

Оператор volatile легко забыть, и трудно понять, зачем он нужен. Без volatile этот код больше не был бы потокобезопасным из-за дважды проверенного антипаттерна блокировки. Подробнее об этом читайте в параграфе 16.2.4 книги Параллелизм Java на практике. Вкратце: этот шаблон (до Java 5.0 или без оператора volatile) мог возвращать ссылку на объект Bar, который (все еще) находится в некорректном состоянии.

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

class Bar {
private static class BarHolder {
public static Bar bar = new Bar();
}

public static Bar getBar() {
return BarHolder.bar;
}
}
java