Есть ли у Java фиаско инициализации статического порядка? - PullRequest
10 голосов
/ 07 июля 2011

Недавний вопрос здесь содержал следующий код (ну, похожий на этот) для реализации синглтона без синхронизации.

public class Singleton {
    private Singleton() {}
    private static class SingletonHolder { 
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Теперь, я думаю, понимаю, что это делает. Поскольку экземпляр является static final, он создается задолго до того, как какой-либо поток вызовет getInstance(), поэтому в действительности нет необходимости в синхронизации.

Синхронизация потребовалась бы только в том случае, если два потока пытались вызвать getInstance() одновременно (и этот метод создавался при первом вызове, а не во "static final" времени).

Поэтому мой вопрос в основном так: почему тогда вы бы предпочли ленивую конструкцию синглтона с чем-то вроде:

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

Я думал только о том, что при использовании метода static final может возникнуть проблема секвенирования, как в фиаско статического порядка инициализации C ++.

Прежде всего, в Java есть эта проблема? Я знаю, что порядок в пределах полностью указан, но гарантирует ли это какой-либо последовательный порядок между классами (например, с помощью загрузчика классов)?

Во-вторых, если порядок является непротиворечивым, то почему бы ленивый вариант строительства был бы выгоден?

Ответы [ 11 ]

13 голосов
/ 07 июля 2011

Теперь, я думаю, понимаю, что это делает. Поскольку экземпляр статический final, он создается задолго до того, как какой-либо поток вызовет getInstance (), поэтому в синхронизации нет реальной необходимости.

Не совсем. Он создается, когда класс SingletonHolder инициализирован , что происходит при первом вызове getInstance. Загрузчик классов имеет отдельный механизм блокировки, но после загрузки класса дальнейшая блокировка не требуется, поэтому эта схема выполняет достаточно блокировки, чтобы предотвратить многократное создание экземпляров.

Прежде всего, есть ли у Java эта проблема? Я знаю, что порядок внутри класса полностью указан, но гарантирует ли это каким-то образом согласованный порядок между классами (например, с помощью загрузчика классов)?

У Java есть проблема, когда цикл инициализации класса может привести к тому, что некоторый класс будет наблюдать статические финалы другого класса до их инициализации (технически до того, как все статические блоки инициализатора будут выполнены).

Рассмотрим

class A {
  static final int X = B.Y;
  // Call to Math.min defeats constant inlining
  static final int Y = Math.min(42, 43);
}

class B {
  static final int X = A.Y;
  static final int Y = Math.min(42, 43);
}

public class C {
  public static void main(String[] argv) {
    System.err.println("A.X=" + A.X + ", A.Y=" + A.Y);
    System.err.println("B.X=" + B.X + ", B.Y=" + B.Y);
  }
}

Беглые отпечатки С

A.X=42, A.Y=42
B.X=0, B.Y=42

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

3 голосов
/ 07 июля 2011

Теперь, я думаю, понимаю, что это делает.Поскольку экземпляр статический final, он создается задолго до того, как какой-либо поток вызовет getInstance (), поэтому в синхронизации нет реальной необходимости.

Нет.Класс SingletonHolder будет загружен только при первом вызове SingletonHolder.INSTANCE.final Объект станет видимым для других потоков только после того, как он полностью построен.Такая ленивая инициализация называется Initialization on demand holder idiom.

1 голос
/ 07 июля 2011

Паттерн, который вы описали, работает по двум причинам

  1. Класс загружается и инициализируется при первом обращении к нему (через SingletonHolder.INSTANCE здесь)
  2. Загрузка и инициализация класса является атомарной в Java

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

1 голос
/ 07 июля 2011

В Эффективная Java , Джошуа Блох отмечает, что «Эта идиома … использует гарантию, что класс не будет инициализирован, пока он не будет использован [ JLS, 12.4.1 ]. "

0 голосов
/ 01 февраля 2012

Единственный правильный синглетон в Java может быть объявлен не классом, а перечислением:

public enum Singleton{
   INST;
   ... all other stuff from the class, including the private constructor
}

Используется как:

Singleton reference1ToSingleton=Singleton.INST;    

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

0 голосов
/ 14 июля 2011

Мне не нравится ваш код, но у меня есть ответ на ваш вопрос.Да, у Java есть фиаско порядка инициализации.Я наткнулся на это с взаимозависимыми перечислениями.Пример может выглядеть следующим образом:

enum A {
  A1(B.B1);
  private final B b;
  A(B b) { this.b = b; }
  B getB() { return b; }
}

enum B {
  B1(A.A1);
  private final A a;
  B(A a) { this.a = a; }
  A getA() { return a; }
}

Ключ заключается в том, что B.B1 должен существовать при создании экземпляра A.A1.И для создания A.A1 должен существовать B.B1.

Мой реальный пример использования был немного более сложным - отношения между перечислениями были на самом деле parent-child, поэтому одно перечисление возвращало ссылку на своего родителяно второй массив его потомков.Дети были частными статическими полями перечисления.Интересно то, что при разработке под Windows все работало нормально, но в производственной среде (то есть Solaris) члены дочернего массива были нулевыми.Массив имел правильный размер, но его элементы были нулевыми, потому что они не были доступны при создании экземпляра массива.

Так что я закончил с синхронизированной инициализацией при первом вызове.: -)

0 голосов
/ 07 июля 2011

Прежде всего, есть ли у Java эта проблема?Я знаю, что порядок внутри класса полностью указан, но гарантирует ли это какой-либо согласованный порядок между классами (например, с помощью загрузчика классов)?

Да, но в меньшей степени, чем в C ++:

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

  • Если в статической инициализации цикла существует цикл зависимостигруппа классов, то порядок инициализации классов является неопределенным.

  • Однако Java гарантирует, что инициализация статических полей по умолчанию (на ноль / ноль / ложь) произойдет до того, как какой-либо код получитчтобы увидеть значения полей.Таким образом, класс (в теории) может быть написан так, чтобы он делал правильные вещи независимо от порядка инициализации.

Во-вторых, если порядок согласован, то зачем использовать ленивую конструкциюкогда-нибудь быть выгодным?

Ленивая инициализация полезна в ряде ситуаций:

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

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

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

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

0 голосов
/ 07 июля 2011

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

«Доступ» здесь относится к ограниченным действиям , указанным в спецификации .В следующем разделе говорится об инициализации.

То, что происходит в вашем первом примере, эквивалентно

public static Singleton getSingleton()
{
    synchronized( SingletonHolder.class )
    {
        if( ! inited (SingletonHolder.class) )
            init( SingletonHolder.class );
    } 
    return SingletonHolder.INSTANCE;
}

(После инициализации блок синхронизации становится бесполезным; JVM его оптимизирует).

Семантически это не отличается от 2-го импл.Это на самом деле не затмевает «двойную проверку блокировки», потому что - это двойная проверка блокировки.

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

0 голосов
/ 07 июля 2011

Код в первой версии - это правильный и лучший способ безопасного ленивого создания синглтона.Модель памяти Java гарантирует, что INSTANCE будет:

  • Инициализируется только при первом использовании (т. Е. Ленивым), поскольку классы загружаются только при первом использовании
  • . Создаются ровно один разполностью поточно-ориентированный, потому что вся статическая инициализация гарантированно будет завершена до того, как класс станет доступным для использования

Версия 1 - отличный образец для подражания.

EDITED
Версия 2 является поточно-ориентированной, но немного дорогой и, что более важно, серьезно ограничивает параллелизм / пропускную способность

0 голосов
/ 07 июля 2011

Просто небольшое замечание о первой реализации: здесь интересно то, что инициализация класса используется вместо классической синхронизации.

Инициализация класса очень хорошо определена, так как ни один код не может получить доступ ни к чему изкласс, если он не полностью инициализирован (т.е. весь статический код инициализатора был выполнен).А поскольку к уже загруженному классу можно получить доступ с нулевыми накладными расходами, это ограничивает накладные расходы на «синхронизацию» в тех случаях, когда необходимо выполнить фактическую проверку (т. Е. «Класс уже загружен / инициализирован?»).

Один из недостатков использования механизма загрузки классов заключается в том, что его трудно отладить, когда он ломается.Если по какой-то причине конструктор Singleton выдает исключение, то первый вызывающий абонент на getInstance() получит это исключение (обернутое в другое).

второй абонент, однако, никогда не увидит причину проблемы (он просто получит NoClassDefFoundError).Так что если первый абонент как-то игнорирует проблему, то вы никогда не сможете узнать , что именно пошло не так.

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

...