Pull to refresh

Comments 84

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

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

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

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

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

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

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

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

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

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

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

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

Википедия со ссылкой на Joshua Bloch «Effective Java, Second Edition», p. 283-284 предлагает избегать лишних чтений volatile полей, якобы это вредит перформансу:

en.wikipedia.org/wiki/Double-checked_locking

// Works with acquire/release semantics for volatile in Java 1.5 and later
// Broken under Java 1.4 and earlier semantics for volatile
class Foo {
    private volatile Helper helper;
    public Helper getHelper() {
        Helper localRef = helper;
        if (localRef == null) {
            synchronized (this) {
                localRef = helper;
                if (localRef == null) {
                    helper = localRef = new Helper();
                }
            }
        }
        return localRef;
    }

    // other functions and members...
}


«Note the local variable „localRef“, which seems unnecessary. The effect of this is that in cases where helper is already initialized (i.e., most of the time), the volatile field is only accessed once (due to „return localRef;“ instead of „return helper;“), which can improve the method's overall performance by as much as 25 percent.[7]»

Противоречий нет конечно, возможно они имели ввиду как раз таки общий случай, а не x86, но тогда вопрос, если на x86 мы гарантировано не получаем прироста производительности, то откуда они взяли цифру 25% и на какой платформе они получили такой прирост, и хорошо бы им это упомянуть, в силу того, что x86 всё таки значительно популярнее всего остального.
Не отличается на уровне архитектуры — в том смысле, что используется точно такая же mov инструкция, — но на уровне компилятора, естественно, отличается. Два чтения volatile поля всегда будут двумя операциями чтения из памяти, и компилятор не имеет права это оптимизировать. В случае с не-volatile полем второе чтение может быть оптимизировано.
Возможно тут вы найдете ответы:

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 и т.д.).
В enum варианте какой-то странный плюс «Поддержка switch» — зачем может быть нужен switch из одного варианта?
Да и «Остроумно» я бы скорее отнёс к минусам :)
Вывод: если правильно подобрать реализацию шаблона можно получить ускорение (speed up) от 2х до 4х.

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

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

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

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

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

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

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

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

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

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

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

Говорил про свинг не просто так. Долго ковырял его и находил «неправильные синглтоны». На сколько часто они используются сказать не могу.
В свинге прирост производительности будет на эти самые 0.000001% которые даже измерить трудно. Да и в других областях также.
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 сервер приложений, где синглтон нужно реализовывать иначе.
За что минус? Разве я ошибся? В приведенном коде 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.

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

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;
}

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

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

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

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

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

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

Конструкторы писать, статья все-таки не для новичка (новчки прочитают и на википедии). Статья ориентировалась на людей, которые использовали шаблон и у них остались вопросы о том, почему он не работает.

Вот какраз хотел написать в чем же разница между этими примерами и статичными классами. Уже было подумал что очередной хайп

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

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

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

private static final Object lock = new Object();

Иначе кто-то другой может залочиться на Singleton.class, и быть беде.
Правильный синглтон — это оксюморон.
А чем плох дедовский вариант из школьных учебников, когда вся логика читабельна в геттере и который поддерживается и самыми древними 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;
	}
}
Я считаю, что
private static final Object lock = new Object();

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

Вот статья на тему того, как сделать абслютный синглтон, избегающий этой проблемы: surguy.net/articles/communication-across-classloaders.xml
Синглетон вообще порой называют даже антипатерном из-за распределенных систем, кластеров и пр…
Что-то у меня не открывается вторая ссылка…
кстати в модном варианте «3 On Demand Holder idiom» — final не нужен, оно и так статическом инициализаторе все просетит безопасно.
Мой опыт разработок на яве не очень велик, но я несколько раз пытался приладить одиночку к моим проектам и каждый раз понимал что это совершенно бессмысленно, возвращаля к обычным статическим классам. Максимум что я могу себе представить это разная работа с памятью в потоках и более «правильная» уборка мусора. Буду рад примеру реального применения этого паттерна
Тоже хотел бы получить развернутый ответ на вопрос: «чем статический класс хуже синглтона, кроме не ленивой инициализации?».

public static class SingletonHolder

Похоже что он должен быть private - смысл в том что ссылки на него должны существовать в программе только в рамках метода getInstance

- Невозможно использовать для не статических полей класса

Может кто-нибудь пояснить что это значит? Вообще не понял что имелось ввиду.

Sign up to leave a comment.

Articles

Change theme settings