Пользователь
0,0
рейтинг
30 сентября 2011 в 14:22

Разработка → Правильный Singleton в Java

JAVA*
Уверен, каждый из читателей, знает что такое шаблон проектирования “Singleton”, но не каждый знает как его программировать эффективно и правильно. Данная статья является попыткой агрегирования существующих знаний по этому вопросу.

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

Неленивый Singleton в Java

Автору известно два способа реализации шаблона с нормальной инициализацией.

1 Static field

public class Singleton {
	public static final Singleton INSTANCE = new Singleton();
}

+ Простая и прозрачная реализация
+ Потокобезопасность
- Не ленивая инициализация

2 Enum Singleton

По мнению Joshua Bloch’а это лучший способ реализации шаблона [1].

public enum Singleton {
	INSTANCE;
}

+ Остроумно
+ Сериализация из коробки
+ Потокобезопасность из коробки
+ Возможность использования EnumSet, EnumMap и т.д.
+ Поддержка switch
- Не ленивая инициализация

Ленивый Singleton в Java

На момент написания статьи существует как минимум три корректных реализации шаблона Singleton с ленивой инициализацией на Java.

1 Synchronized Accessor

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

+ Ленивая инициализация
- Низкая производительность (критическая секция) в наиболее типичном доступе

2 Double Checked Locking & volatile

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

+ Ленивая инициализация
+ Высокая производительность
- Поддерживается только с JDK 1.5 [5]

2.1 Почему не работает без volatile?

Проблема идиомы Double Checked Lock заключается в модели памяти Java, точнее в порядке создания объектов. Можно условно представить этот порядок следующими этапами [2, 3]:

Пусть мы создаем нового студента: Student s = new Student(), тогда

1) local_ptr = malloc(sizeof(Student)) // выделение памяти под сам объект;
2) s = local_ptr // инициализация указателя;
3) Student::ctor(s); // конструирование объекта (инициализация полей);

Таким образом, между вторым и третьим этапом возможна ситуация, при которой другой поток может получить и начать использовать (на основании условия, что указатель не нулевой) не полностью сконструированный объект. На самом деле, эта проблема была частично решена в JDK 1.5 [5], однако авторы JSR-133 [5] рекомендуют использовать voloatile для Double Cheсked Lock. Более того, их отношение к подобным вещам легко прослеживается из коментария к спецификации:

There exist a number of common but dubious coding idioms, such as the double-checked locking idiom, that are proposed to allow threads to communicate without synchronization. Almost all such idioms are invalid under the existing semantics, and are expected to remain invalid under the proposed semantics.

Таким образом, хотя проблема и решена, использовать Double Checked Lock без volatile крайне опасно. В некоторых случаях, зависящих от реализации JVM, операционной среды, планировщика и т.д., такой подход может не работать. Однако, серией опытов сопровождаемых просмотром ассемблерного кода, генерированного JIT’ом автору, такой случай вопросизвести не удалось.

Наконец, Double Checked Lock можно использовать без исключений с immutable объектами (String, Integer, Float, и т.д.).

3 On Demand Holder idiom

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


+ Ленивая инициализация
+ Высокая производительность
- Невозможно использовать для не статических полей класса

Performance

Для сравнения производительности выше рассмотренных методов, была использована микро-бенчмарка [6], определяющая количество элементарных операций (инкремент поля) в секунду над Singleton объектом, из двух параллельных потоков.

Измерения производились на двухядерной машине Intel Core 2 Duo T7300 2GHz, 2Gb ram и Java HotSpot(TM) Client VM (build 17.0-b17). За единицу скора считается количество инкрементов (а следовательно и захватов объекта) в секунду * 100 000.

(больше — лучше)
Client Server
Synchronized accessor 42,6 86,3
Double Checked Lock & volatile 179,8 202,4
On Demand Holder 181,6 202,7


Вывод: если правильно подобрать реализацию шаблона можно получить ускорение (speed up) от до .

Summary

Можно выделить следующие короткие советы по использованию того или иного подхода для реализации шаблона “Одиночка” [1].

1) Использовать нормальную (не ленивую) инициализацию везде где это возможно;
2) Для статических полей использовать On Demand Holder idom;
3) Для простых полей использовать Double Chedked Lock & volatile idom;
4) Во всех остальных случаях использовать Syncronized accessor;

Java Class Library & Singleton

Примечательно, что разработчики Java Class Library выбрали наиболее простой способ реализации шаблона — Syncronized Accessor. C одной стороны — это гарантия совместимости и правильной работы. С другой — это потеря процессорного времени на вход и выход из критической секции при каждом обращении.

Быстрый поиск grep’ом по исходникам дал понять, что таких мест в JCL очень много.

Возможно следующая статья будет “Что будет если в Java Class Library правильно написать все Singleton классы?” :)

Links
[1] Joshua Bloch, Effective Java Reloaded talk at Google I/O 2008 (video);
[2] Double-checked locking and the Singleton pattern;
[3] The «Double-Checked Locking is Broken» Declaration;
[4] en.wikipedia.org/wiki/Double-checked_locking
[5] JSR-133
[6] How to write microbenchmarks
Костюков Владимир @spiff
карма
242,0
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (78)

  • +2
    Правильный Singleton пишется без 'e' на конце.
    И чем же, по-вашему, вариант с enum не «ленивый»?
    • 0
      О, спасибо. Везде исправил.

      По моему вариант с enum не «ленивый». Т.е. Объект создастся сразу, как только загрузится клаас. А если например, у нас есть статические методы в этом enume? Вызвав их хоть раз, мы создадим инстанс. Получается не ленивый.
      • +3
        Это распространенное заблуждение. Объект создастся не тогда, когда загрузится класс, а когда класс будет инициализирован. А инициализация класса произойдет в момент первого доступа к INSTANCE. А если у класса-синглтона вызывают прочие статические методы, которым не нужен этот самый instance, на мой взгляд, это говорит о недоработке в проектировании.
        • 0
          Да, все верно. Только для меня ленивая инициализация значит «отложеная инициализация объекта, после инициализации класса» (приминительно к синглтону). Т.е. эти вещи надо разделять, а напроектировать можно что угодно.

          Перечитал Блоха, ссылки. Ни где не нашел упоминания о том, что вариант с Enum — ленивый.
        • +1
          По-моему, не в момент доступа к INSTANCE, а в момент доступа к классу Singletone, что есть весьма разные вещи. Хотя это очень близко к ленивой инициализации и во многих случаях может её заменить.
  • 0
    А как же AtomicReference?
    • 0
      Не позволит избежать создания лишнего объекта.
  • +5
    Интересно, но эх, я уже давным-давно делаю синглтоны вот так:

    @javax.inject.Singleton
    public class MyClass {… }

    :)
    • 0
      А как это работает?
      • 0
        Честно — не знаю как именно. Я просто верю контейнеру (spring/guice/jee), что он делает этот синглтон правильно. Никаких проблем не было, так что я особо и не вникал
        • 0
          Не, это я понимаю
          Но что должно быть внутри класса? Как позаботиться о том, что в коде его не создадут несколько раз?
          • +1
            Согласно DI вообще не нужно создавать инстансы вручную. Просто соглашение, что все зависимости в коде не создаются, а тянутся извне через аннотацию @Inject, т.е. примерно так:

            @javax.inject.Singleton
            public class MyFirstClass {… }

            • +1
              Сорри, отправилось нечайно.

              @javax.inject.Singleton
              public class MyFirstClass {… }

              @javax.inject.Singleton
              public class MySeconfClass {
              @Inject private MyFirstClass obj;
              }
              • +1
                Но в полноценном, многопоточном энтерпрайзе мне кажется более логично использовать
                @javax.ejb.Singleton
            • 0
              А, я понял. То есть тут речь идет о полноценном энтерпрайзе, а не об использовании отдельно взятой аннотации.
              • 0
                Просто эту аннотацию уже все распространенные технологии поддерживают, это, можно сказать, стандарт.
        • –1
          И снова таки в ранних версиях jdk без аннотаций, работать не будет.
          • +2
            В 1.4 что ли и ранее? :) Для легаси систем да. Но сейчас уже 6-я жава мейнстрим и 7-я потихоньку в прод внедряется…
            • –1
              На работе работал пол года назад с двумя проектами Java 1.4 и один переходил только на 1.5. О чём ещё говорить?
              • +1
                О том, что:

                1) Глупо писать новый проект на Java < 1.5 :)
                2) Проектов на < 1.5 все меньше и меньше

                С уважением, КЭП.
                • –2
                  У заказчика есть деньги и нет мозга. Что посоветует КЭП? А ничего. КЭП исполнитель...
                  И как КЭП сам подметил их всё меньше и меньше, но это не значит что их нет.

                  (С) Человек СПАТЬ. Меня хотят все.
                  • +2
                    Я не пойму, что вы мне пытаетесь доказать.
  • 0
    Понятно почему Double Checked Locking быстрее, чем Synchronized Accessor — синхронайз работает не при каждом получении инстанса, а только однократно. Но не очень понятно почему Synchronized Accessor получается настолько медленнее Double Checked Locking + volatile. Ведь использование volatile в данном контексте и таким образом фактически равнозначно тому, что к ней обращаются через synchronized над ней же, причём точно так же — при каждом вызове getInstance(). Объясните кто-нибудь, пожалуйста.
    • +1
      Чтение volatile поля в Java на x86 архитектуре ничем не отличается от чтения обычного поля. Отличается только запись volatile поля, которая сопровождается инструкцией lock add [esp], 0, служащей эффективным memory-barrier'ом. И то, это совсем далеко от того, что делается при синхронизации с помощью synchronized. В общем, не слушайте тех, кто говорит, что volatile равносилен synchronized. В HotSpot VM накладные расходы на доступ к volatile полям очень маленькие, и то только на запись.
      • 0
        Я был уверен что и на запись и на чтение.
      • 0
        Объясните, пожалуйста, как объявление поля volatile спасает от проверки на null? Или получается, что если оно volatile, то эти 3 инструкции (allocate, assign, constructor) обязаны выполниться ДО любой операции чтения?
        • 0
          Не спасает. В статье пример «2 Double Checked Locking & volatile» написан неправильно.
          • 0
            И действительно неправильно. Сейчас все переписал. Спасибо.
    • +1
      Возможно тут вы найдете ответы:

      www.javaperformancetuning.com/news/qotm051.shtml
      www.javaperformancetuning.com/news/qotm030.shtml

      Если в кратце, синхронизация volatile стоит столько-же или дешевле чем monitor enter/monitor exit. Опять же, думаю играет не малую роль то, что при использовании syncronized приходиться синхронизировать и обновлять все копии переменных (shared variables) — а это нагрузка на кеш L2/память. А volatile перменная — одна в main memory.

      Боллее того, эти числа могут сильно различаться в зависимости от платформы (NUMA, HT и т.д.).
  • +5
    В enum варианте какой-то странный плюс «Поддержка switch» — зачем может быть нужен switch из одного варианта?
    Да и «Остроумно» я бы скорее отнёс к минусам :)
  • +1
    Вывод: если правильно подобрать реализацию шаблона можно получить ускорение (speed up) от 2х до 4х.

    Ускорение чего именно можно получить? Ускорение обращения к классу риспользующему паттерн синглтон? А подобные обращения занимают в реальных приложениях 1% или может быть 0.000001%?

    Примечательно, что разработчики Java Class Library выбрали наиболее простой способ реализации шаблона — Syncronized Accessor.

    Java началась 20 лет назад и основные классы разрабатывались ещё до появления моды на шаблоны проектирования.

    Что будет если в Java Class Library правильно написать все Singleton классы?

    Ничего не будет. Никто даже не заметит.
    • 0
      Не соглашусь — ускорение можно получить. Обращение к подобным классам очень частое. Воспользуйтесь grep'ом для того чтобы убедиться самостоятельно. Почти все классы *Manager — Singleton'ы в JCL.

      Хороший пример того как нужно писать можно посмотреть в классе java.awt.AppContext.

      Да, 20 лет назад. Но это не значит, что не надо развиваться и пытаться исправить ошибки и наследие былых лет.

      Микробенчмарки все заметят :) Особенно мне кажется изменения коснутся Swing/AWT.

  • –1
    Но это не значит, что не надо развиваться и пытаться исправить ошибки и наследие былых лет.

    это не ошибки. Никакого заметного влияния на производительность это не имеет (а уж тем более в Свинге) — значит незачем что-то менять и тратить деньги. Ресурсы на производство ПО ещё 20 лет назад умели считать.

    • 0
      Эксперимент дешевый. Практически достаточно sed'а. Уверен, в Оракле его уже делали, но результаты не публиковали. Да и к тому-же сложно сравнить эффект. На specJVM — очевидно ничего недаст. Других вменяемых ворклоадов нет. А вот формочка может будет и быстрее инициализироваться.

      Говорил про свинг не просто так. Долго ковырял его и находил «неправильные синглтоны». На сколько часто они используются сказать не могу.
      • 0
        В свинге прирост производительности будет на эти самые 0.000001% которые даже измерить трудно. Да и в других областях также.
  • +1
    if (localInstance == null) {
    	synchronized (Singleton.class) {
    		instance = localInstance;
    		if (instance == null) {
    			localInstance = instance = new Singleton();
    		}
    	}
    }
    

    Ошибка в instance = localInstance; ведь между if (localInstance == null) и захватом монитора на Singleton.class другой поток может успеть проинициализировать instance.

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

    Минус «Поддерживается только с JDK 1.5» на самом деле никакой не минус, JDK1.4 уже не поддерживается. А там, где оно все же используется, 99% будет какой-нибудь legacy сервер приложений, где синглтон нужно реализовывать иначе.
    • +3
      За что минус? Разве я ошибся? В приведенном коде new Singleton() может выполниться дважды.

      1	Singleton localInstance = instance;
      2	if (localInstance == null) {
      3		synchronized (Singleton.class) {
      4			instance = localInstance;
      5			if (instance == null) {
      6				localInstance = instance = new Singleton();
      7			}
      8		}
      9	}
      10	return instance;
      


      Пусть instance == null
      Поток 1 выполняет строчку 1 и останавливается, поток 2 получает управление, выполняет строки 1-10 и останавливается, инстанс уже создан. Поток 1 снова получает управление, доходит до строки 4 и перезаписывает поле, уже инициализированное потоком 2 — записывает туда localInstance (которое у потока 1 == null). Далее проверка в строке 5 успешна и выполняется строка 6.

      Что не так?
      • +1
        Я вобще не понимаю зачем тут прикручена локалинстанс? Переменная не несёт смысла, не используется, и только убивает логику. Я вобще себе иначе помню даблчек локинг.

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

        В этом варианте есть локальная переменная, но она создаётся внутри синхронайзд блока, и не убивает логику как тут.
        • 0
          Ваше понимание очень похоже на раздел «A fix that doesn't work»
          www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
          • 0
            Если поле volatile — то 1 в 1 раздел «Fixing Double-Checked Locking using Volatile»
            • 0
              сорри, не 1 в 1, у них как раз таки нет локальной переменной.
            • 0
              Перечитайте указанный вами раздел. Там один synchronized блок и нет дополнительных переменных.
              • 0
                Да, вы правы.
                • 0
                  Вариант с Double Checked Lock был неправильным изначально. Сейчас все исправлено.
          • 0
            Я это привел к тому, что вариант автора статьи какой то кривой как мне кажется. Так как описано в статье, я видел пример без каких либо локальных переменных.
            • 0
              Вы говорите о лишней переменной и сами тут же приводите пример с переменной и лишним synchronized блоком, который еще более запутывает код. Это ничем не лучше кода автора, где переменная не нужна.
              • 0
                Я привел ПРАВИЛЬНЫЙ пример, где локальная переменная не наруешает логики работы. Только и всего.
                • 0
                  Может тогда поясните, зачем нужна эта переменная и второй synchronized?
                  • 0
                    Атавизм идеологии о том что в многопоточном приложении при одновременном доступе можно создать копию объекта и работать с ней вне synchronized блока, чем выполнить все действия над общей переменной но внутри блока, тем самым увеличивается производительность/уменьшается простой, кому как нравится.
                    • 0
                      Извиняюсь за задержку с ответом.
                      Да, благая цель второго synchronized понятна, но, насколько я понимаю статью по ссылке, в этом нет смысла, а без volatile переменной пример вообще неправильный.
                      • 0
                        Вариант с Double Checked Lock был неправильным изначально. Сейчас все исправлено.
        • +1
          Судя по всему, возможно ускорение работы, т.к. в случае не-null поля этот метод два раза прочитает volatile-поле — в if и в return. Типа, как избавляемся от повторного чтения в return-е.

          Зачем нужно присваивание, не ясно.
          В Effective Java на странице 284 есть объяснение про чтение, но нет объяснения про запись в локальную переменную.
          Зачем в нее записывать созданный инстанс?

          Автор, вы собираетесь править ошибку в варианте 2? Или объясните, где я не прав.
  • 0
    Кстати, а как внутри JVM происходит потокобезопасная инициализация класса? Вот если 2 потока обратятся к getInstance() из примера 3, что произойдет?

    Метод ClassLoader.loadClass() — synchronized, но я без понятия, как грузятся классы, если к ним обращаться в коде…
  • +1
    При вызове статического метода есть проверка (так называемый class initialization barrier): если receiver класс не инциализирован, то вызывается его инициализация. §2.17.5 JVM specification говорит о том, что при инициализации класса в первую очередь происходит синхронизация на объекте java.lang.Class, представляющем этот класс.

    Однако фокус состоит в том, что в процессе исполнения Java-приложения после инициализации класса JVM может заново перекомпилировать метод, вызывающий getInstance(), избавившись от ненужного class initialization barrier и оптимизировав таким образом вызов статического метода.
  • 0
    Всё жду, когда же кто-то напишет, о том, что «синглтоны — зло» и в архитектуре «по последней моде» их использование сведено к минимуму, а значит не так важно, как именно мы получаем экземпляр.
    • +1
      Может быть, все — прагматики? Зачем спорить о применимости шаблона? — ведь в конечном счете все зависит от конкретной задачи. По хорошему да, синглтоны не нужны, но раз уж кто-то решил создать у себя в коде синглтон, пусть уж знает, как правильно это делается — вот тут и пригодится статья.

      * Сам за годы работы ни разу не писал синглтонов.
      • 0
        Если «все прагматики», то и темы бы не было — какая разница как получать экземпляр если мы синглотанми почти не пользуемся). Да и HotSpot VM, похоже, разогревшись, уберёт блок синхронизации даже из «Synchronized Accessor», так какая тогда разница «как»?))
      • 0
        Сходу вспоминается DependencyManager в DI, EntityManager в ORM. Даже простое подключение к базе через JNDI тоже, скорее всего, будет сделано через синглтон. Используется повсеместно в своих областях.
      • 0
        Правильно — синглтоны не создавать. Из чистейшего, незамутненного прагматизма.
  • 0
    Во варианте 1 Synchronized Accessor — метод должен быть статический.
    • 0
      Cпасибо, исправил.
      • 0
        Да, еще внешние классы static: компилятор ругается (modifier static not allowed here)
        • 0
          Исправил, спасибо.
  • +1
    По-моему даже в простых примерах стоит писать работающий код. У вас все классы static и private конструкторы упущены. Если первое приведет лишь к ошибке компиляции, второе у иного новичка создаст неверное представление о паттерне.
    • –1
      Спасибо, исправил.

      Конструкторы писать, статья все-таки не для новичка (новчки прочитают и на википедии). Статья ориентировалась на людей, которые использовали шаблон и у них остались вопросы о том, почему он не работает.
  • +2
    Несколько не в тему статьи, но вдруг кто-то незнаком с этой проблемой. Во время чтения поста мне прям покоя не давало еще место из effective java про readResolve() в синглтонах. К вашему примеру это не имеет отношения, потому что у вас класс не имплементирует Serializable. Но часто в процессе развития приложения люди навешивают интерфейсы на синглтоны, потому что их нужно куда нибудь передавать, и тогда всплывает проблема, что он может перестать быть синглтоном, если не переопределить readResolve (). Вот тут, например, про это рассказывается. www.javalobby.org/java/forums/t17491.html

    Мне просто очень нравится задавать этот вопрос на интервью :)
    • 0
      Это так странно, в своей работе принимать более чем сомнительные решения (синглтоны, отягощеннеы ещё и множеством обязанностей), и при этом «очень нравится задавать этот вопрос на интервью :)». Делааа…
      • 0
        Я не понял конструктивного посыла вашего комментария. Что вы хотели им сказать? Что не так с тем, чтобы задавать вопрос на понимание того, как работает сериализация и как с ее помощью можно легко нарушить безопасность простейшего, казалось бы, паттерна? Или что, вы не разу не встречались с кодом, написанным тысячей индийских коллег за 7 лет? Или это все в вас бродит ненависть к паттерну синглтон?

        Не бойтесь, он вам ничего не сделает.
  • +6
    Параноидальная ремарочка — лучше синхронизоваться не на классе, а на приватном статическом локе:

    private static final Object lock = new Object();

    Иначе кто-то другой может залочиться на Singleton.class, и быть беде.
  • –1
    Правильный синглтон — это оксюморон.
  • 0
    А чем плох дедовский вариант из школьных учебников, когда вся логика читабельна в геттере и который поддерживается и самыми древними JVM?
    public class Singleton {
    	private static Singleton instance = null;
    	private static final Object lock = new Object();
    	private static boolean isInitialized = false;
    	
    	public static Singleton getInstance() {
    		if (!isInitialized) {
    			synchronized (lock) {
    				if (instance == null) {
    					instance = new Singleton();
    					isInitialized = true;
    				}
    			}
    		}
    		return instance;
    	}
    }
    • 0
      Я считаю, что
      private static final Object lock = new Object();
      

      здесь лишний, т.к. синхронизировать можно по классу. Но в целом присоединюсь к вопросу, вернее его уточню, зачем в реализации Double Checked Locking & volatile используется локальная переменная Singleton localInstance?
  • +1
    Есть ещё одна тонкость. Синглтон не будет синглтоном, если он загружается разными ClassLoader'ами. java.sun.com/developer/technicalArticles/Programming/singletons/
    Ситуация не такая редкая. Сам с такой столкнулся и выявил проблему только после долгого дебага ;)

    Вот статья на тему того, как сделать абслютный синглтон, избегающий этой проблемы: surguy.net/articles/communication-across-classloaders.xml
    • 0
      Синглетон вообще порой называют даже антипатерном из-за распределенных систем, кластеров и пр…
    • 0
      Что-то у меня не открывается вторая ссылка…
  • 0
    В этой статье приведен интересный вариант Синглтона изначально разработанного для Java.

    dlang.ru/low-lock-singletons-v-d
  • 0
    кстати в модном варианте «3 On Demand Holder idiom» — final не нужен, оно и так статическом инициализаторе все просетит безопасно.

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