Pull to refresh

Comments 73

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

В таком случае статический класс, или абстрактный класс — тоже антипаттерн? Он же кроме полезных методов не дает создавать свои экземпляры. По-моему паттерны — это вещи более фундаментальные, чем ответственности классов. Ответственность класса — это спектр возможных действий, совершаемых благодаря вызовам метода класса. Единственный ли при этом экземпляр класса или нет, всем ли он доступен или нет и пр. — это уже не относится к методам класса и его ответственности, это построение архитектуры (которое естественно затрагивает в небольшой степени код класса).
Абстрактный класс не антипаттерн, но без него легко обойтись, так как он использует малополезное наследование реализации.
Статический класс не антипаттерн, это на самом деле вообще не класс, а просто пространство имен.

> По-моему паттерны — это вещи более фундаментальные, чем ответственности классов.
Это в корне неверно. Паттерн принципиально не фундаментальная, а чисто прикладная сущность. Просто кулинарный рецепт. В случае синглтона — рецепт отравы.

>Единственный ли при этом экземпляр класса или нет, всем ли он доступен или нет и пр. — это уже не относится к методам класса
Именно это неверно для синглтона. У него контролем числа экземпляров занимается реализация (методы) класса
Наверно я плохо выразил свою мысль.
Представьте, что программа — это предприятие. В нем есть работники, каждый работник — это класс. Вот работник Вася, он сантехник, и чинит краны. Вот работник Петя, он поливает цветы. Каждый занят своим делом, одной ответственностью. Плохо, если Вася будет и чинить краны, и поливать цветы, поэтому в должностных инструкциях каждого прописано, что он должен делать.

У предприятия есть слой выше — администрация. Они сами не работают, а управляют работой других (это всевозможные DI-контейнеры и пр.). Появляется бизнес-правило: в один момент времени Вася должен разговаривать только с одним человеком (быть синглетоном). Можно приставить ему администратора, который будет за этим следить, но дешевле и проще сделать это частью Васиных обязанностей. НО! Это не нарушение разделения ответственности! Вася делает ровно то, что и раньше, изменение чисто организационное. Это другая плоскость, другое измерение, нельзя это смешивать, и говорить, что у Васи теперь 2 задачи.
>Наверно я плохо выразил свою мысль.
Мысль вполне понятна, но очевидно неверна.

>В нем есть работники, каждый работник — это класс
Каждый работник — это объект, а не класс.

>Появляется бизнес-правило: в один момент времени Вася должен разговаривать только с одним человеком (быть синглетоном)
Это не синглтон, это однозадачность.

>Можно приставить ему администратора, который будет за этим следить, но дешевле и проще сделать это частью Васиных обязанностей.
В программировании дешевле и проще приставить администратора, тем более что их вагон готовых и отлаженных.
В реальной жизни (почта, сбербанк) при сколько-нибудь высоких нагрузках дешевле и проще… приставить администратора, получив английскую очередь, если администратор живой, или электронную, если это терминал, выдающий талончики. При пиках нагрузки оптимальны будут и терминал, и девочки в зале. Именно такая организация позволяет лучшим отделениям сбера пропускать колоссальный поток клиентов.

>Это другая плоскость, другое измерение, нельзя это смешивать, и говорить, что у Васи теперь 2 задачи.
Это другая плоскость и этим должен заниматься другой класс. Говорить Васе, что у него не прибавилось обязанностей, значит дезинформировать. Будь Вася человеком, он бы подумал, что его пытаются нагрузить на халяву.
Простите что вмешиваюсь со своими дилетантскими рассуждениями, но

Делать Васю синглтоном действительно не вполне понятно зачем: сантехник он и есть сантехник.
Но у того же самого предприятия есть, например, начальник, синглтон по определению, причём не важно кто конкретно занимает должность — Василий Петрович или Андрей Степанович: как структура предприятия, так и «встроенные функции» начальника гарантируют его единственность на данном вверенном участке.

Иначе получаем непредвиденное поведение что предприятия, что программы в лучшем случае.

Нет?
Не совсем. Встроенный свойства Василия Петровича не говорят только о том что он единственный. А вот организационная схема гарантирует его единственность в пределах контекста — предприятия например. Но он отнюдь не единственный начальник в глобальном скопе — холдинге.
При следующем рефакторинге у начальника оказывается свой начальник, а Вася иногда бывает начальником своих подмастериев. Вот тогда наступает ад, которого легко избежать не используя синглтоны.
Хорошо, а так?: любая данная конкретная должность начальника гарантирует единственность исполнителя. Если она этого не делает — то очевидно, что дела на предприятии швах.

Что не так?
Это уже частности. Так можно многие вещи оправдать. В общем же без синглтона можно легко обойтись. Так что причин его вводить в общем-то и нет.
Дык любой проект — это и есть сумма всяких частностей. И если для определённого класса этих частностей уже существует готовое решение — зачем изобретать велосипед, и доверять обеспечивать слежение за единственностью Василия Петровича какой-нибудь вневедомственной охране?
Какие причины его не вводить?
Затем что при запуске предприяти (отладке, тестах) начальником может быть поставлен Инван Мокевич, А при переводе предприятия в рабочий режим начальника поставили Василия Ивановича, а потом когда предприятие было продано WorldEvilCorp начальником повесили Джона Паттерсона.

И никто из этих начальников не сидел вцепивишись всем чем можно в кресло. А был поставлен топменджментом и ни кем кроме него меняться не мог. А уникальность гарантировалось тем что кресло всего одно и начльников кто попало не назначал.

Что касается готового решения. DI фреймвороков море. Бери любой какой нравиться больше и пользуйся.
Если у вас многопоточное приложение, которое требует Singleton, почему бы не отказаться от ленивой инициализации (если найдется хотя бы один пример при котором она действительно необходима, то у вас явно что-то спроектировано не так) убрать весь этот громоздкий код, медленные синхронизации и жить счастливо?
В таком случае еще проще отказаться от синглтона, создать экземпляр при старте и раздавать его всем страждущим объектам
или использовать DI и писать код, который легко тестируется и поддерживается
В этом собственно сущность DI и есть — раздать всем страждущим по зависимостям их.
Я вообще не понимаю, почему еще кто-то в здравом уме и твердой памяти может всерьез рассматривать синглтон не как антипаттерн, особенно в таком ООП-мире как Java
Вот не надо про здравый ум) когда синглтон придумывали небыло всех этих ваших DI и смотрелся он вполне нормально, а сейчас это пережиток прошлого
Это точно большинство старых проектов как то живут, вон гляньте на Eclipse, Xamarin(MonoDevelop), что не сервис то синглтон.
Про здравый ум очень даже надо — сейчас не начало нулевых, а середина десятых.
Но что мы имеем?
1. Обсуждаемый пост о невозможном «правильном синглтоне»
2. Ссылку на свежую (совсем недавно правленую) статью в вике, где синглтон — вполне себе паттерн.
3. Ссылку на хабрапосты от 2011 и 2008 года.
4. Ссылку на вопрос в StackOverflow от 2008 года.
Хотя формирование принципов SOLID поставило синглтон под сомнение, а DI и вовсе сделало очевидным, что это антипаттерн.
Так что если банду четырех и разработчиков легаси понять можно, то тех кто пишет синглтоны сейчас — нельзя.
Хуже всего — те, кто учат писать синглтоны.

Сейчас другая проблема, как мне кажется. Если у вас в руках молоток, то все проблемы начинают напоминать гвозди. Раньше все пихали паттерны куда нужно и куда не нужно — лишь бы побольше паттернов. Сейчас то же самое происходит с DI и другими модными штуками. Но и то и другое — не абсолютное добро.
Вспомнилось «Эволюция кода в зависимости от опыта программиста»:
vk.com/wall-30666517_796260
Я не очень понял про DI, объясните пожалуйста что именно в имеете в виду, архитектурный принцип или средства управления зависимостями? Какие средства используете вы для внедрения зависимостей?
Принцип — класс бизнес-логики не отвечает за композицию. Средства уже вторичны.
Синглтон сам по себе ни в чем не виноват. Все зло, которое приписывают синглтону, происходит от наличия глобальной переменной (или метода-геттера). Синглтон нужен только для определения единственности объекта, доступ к нему — на совести программиста. Никто не мешает создать синглтон и пропихивать его как зависимость, например, в конструкторы.
>Никто не мешает создать синглтон и пропихивать его как зависимость, например, в конструкторы.
И это уже не будет синглтоном, так как за единственность экземпляра в данном случае отвечает уже не сам класс, а тот, кто пропихивает.
Так что «виноват» именно синглтон — он берет на себя ответственность организационного уровня по определению.
Ээ нет. Вы не сможете создать еще один экземпляр класса, реализующего паттерн синглтон. Просто потому что конструктор у него приватный. Плюс с другой стороны, клиенту синглтона абсолютно пофиг, синглтон он или нет — это просто объект нужного ему класса.
На организационном уровне паттерн синглтон не нужен — единственность обеспечивается классом-организатором.
На уровне клиента паттерн синглтон тоже не нужен, как вы уже сами заметили.
Следовательно, если можно «создать и пропихивать как зависимость», то синглтон не нужен от слова совсем.
А с приватным конструктором встает ребром вопрос о зависимостях самого синглтона со всеми вытекающими.
Согласен. Все глобальные объекты инициализируешь до того, как потоки начнут с ними работать. И никакие синглтоны не нужны.

Либо можно забить на то, что при определенных обстоятельствах вдруг родится не один экземпляр а целых два.
>> Либо можно забить на то, что при определенных обстоятельствах вдруг родится не один экземпляр а целых два.

Неа, нельзя так забивать, тем более когда много потоков — можно таких чудесных багов поиметь, что потом будет очень больно. Представьте, что у вас 100 000 потоков, которые делают банковскую транзакию сол счета пользователя на ваш счет с разными суммами, и вот представьте, что из-за неправильной инициализации всего 3 потока взяли неправильный номер счета(для простоты NULL), и вот у вас 99 997 транзакция по копейке, а 3 транзакции по 100 000 денег — ушли в никуда.

В вашем примере нельзя так делать, зато бывает много ситуаций, когда создание лишнего экземпляра не вызывает проблем.
да, если instance1.eqauls(instance2) или instance1 == instance2
Enum — лучший синглтон.
Джошуа Блох.
Этот код делает то же самое, но гораздо проще для понимания:

public class Something {
    private Something() {}
 
    private static class LazyHolder {
        private static final Something INSTANCE = new Something();
    }
 
    public static Something getInstance() {
        return LazyHolder.INSTANCE;
    }
}
А что будет, если из конструктора Something вылетит исключение?
Между прочим, зря минусуете. На днях была подобная ситуация, вылетал по-крупному с NoClassDefFoundError. Думали на ошибку в сборке или ClassLoader. Причем оригинальный Exception не содержался в stacktrace. Поэтому в этом случае правильный код будет не такой тривиальный:

    private static class LazyHolder {
        private static final Something INSTANCE;
        private static final Exception EXCEPTION;
        static {
          try {
            INSTANCE = new Something();
            EXCEPTION = null;
          } catch ( Exception ex ) {
            INSTANCE = null; EXCEPTION = ex;
          }
        }
    }

    public static Something getInstance() throws Exception {
        if ( LazyHolder.INSTANCE != null )
          return LazyHolder.INSTANCE
        else throw EXCEPTION;
    }
Я ниже запостил пример — все там нормально с исключениями.
Что значит, «нормально»? Обработать ошибку и повторно вызывать getInstance() уже не получится.
А, вы про это. Я думал, вы имели ввиду, что исключение теряется и его не видно.
Да, в этом случае так и есть — второй раз будет NoClassDefFoundError

С другой стороны, часто вы обрабатываете ошибки при создании синглетонов? :)
Эта ветка дискуссии началась с того, что ваш пример делает то же самое. Я просто показал, что различия в поведении наших классов все-таки есть.
Он и делает то же самое — в известных пределах.
Очевидно, что любой паттерн — это не закон, а просто способ решить какую-то задачу.
Если стоит задача отлавливать ошибки при создании синглетона — то мой вариант не подходит.
Если такой задачи не стоит — то ваш пример слишком громоздкий.
Отсюда и вопрос — часто ли приходится отлавливать ошибки при создании синглетонов :)
public class SingletonHolder {
    private SingletonHolder() {
        throw new RuntimeException("test exception");
    }

    private static class LazyHolder {
        private static final SingletonHolder INSTANCE = new SingletonHolder();
    }

    public static SingletonHolder getInstance() {
        return LazyHolder.INSTANCE;
    }

    public static void main(String[] args) {
        SingletonHolder instance = SingletonHolder.getInstance();
        System.out.println(instance);
    }
}


Exception in thread "main" java.lang.ExceptionInInitializerError
	at org.relgames.test.SingletonHolder.getInstance(SingletonHolder.java:16)
	at org.relgames.test.SingletonHolder.main(SingletonHolder.java:20)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:606)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
Caused by: java.lang.RuntimeException: test exception
	at org.relgames.test.SingletonHolder.<init>(SingletonHolder.java:8)
	at org.relgames.test.SingletonHolder.<init>(SingletonHolder.java:6)
	at org.relgames.test.SingletonHolder$LazyHolder.<clinit>(SingletonHolder.java:12)
	... 7 more

Зря вы, наверное, акцентировали внимание на «одиночке». Щас апологеты «правильного» кода вас подвергнут обструкции.

Этот приём, вообще-то, прекрасно подходит для более общего случая. А именно для отложенной («ленивой») инициализации полей в многопоточном приложении. И поля эти — не обязательно статические.
Да, я уже сам пожалел, что выбрал синглтон для примера.

Вы не знаете, есть ли у данного способа устоявшееся название? Я его назвал function pointer, потому что его на C можно реализовать при помощи указателей на функции. Среди объектно-ориентированных паттернов мне почему-то ничего похожего не попадалось.
Не знаю про название. Если честно, то для меня такой приём в новинку. Но, наверное, в терминологии Java уместнее говорить о фабрике, а не об указателе на функцию.

Предпочитаю в си-шном коде использовать double-checked locking (с барьерами памяти). Это позволяет модифицировать только само поле, а указатель на функцию не менять. Возможно, так будет чуть быстрее: предсказание переходов лучше сработает.
Это паттерн state, вообще говоря.
Правда что ли?

Не узнал.
1. Есть шанс (и очень высокий), что потоки увидят сразу обновленное поле accessor и никогда не возьмут лок. То есть у них гарантированно не будет happens-before с таким синглетоном. Со всеми вытекающими. Классов, которых можно небезопасно публиковать крайне мало. Скажем, даже AtomicBoolean/Long/Integer уже не такие. new AtomicBoolean(true) может вернуть false, даже если его потом никогда не меняли. Впрочем, есть мысли исправить это в 9й яве: cs.oswego.edu/pipermail/concurrency-interest/2013-November/011966.html

2. Про предупреждение Eclipse это вообще похоже на какой-то бред. Видимость поля, наверное, может иметь какое-то влияние на производительность, но это будет крайне экзотический синтетический тест или какая-то нестандартная JVM. Печально, если Эклипс навязывает такие плохие практики.

3. Ну и странно говорить о правильном использовании антипаттерна. Не бывает правильного singleton. Любое его использование это костыль.

Ну и как итог. По моему, вы придумали, как можно написать стандартный плохой double checking singleton idiom, но очень плохо читаемый и многословный. По сути ваш код делает ровно тоже самое, просто сложнее и потому сложнее увидеть проблемы.
1. Вы полагаете, что в каком-нибудь потоке будет ссылка на не до конца проинициализированный экземпляр целевого класса?

2. Вот, что Eclipse мне пишет: Read access to enclosing field TestClass.testField is emulated by a synthetic accessor method. Change visibility of 'testField' to default. Никогда не вдавался в подробности, что это такое, апросто ставил видимость default.

3. Согласен.
1. Почти. Там много всяких спецэффектов возникает. Исключениями будут скажем иммутабл классы где все поля final. Такие классы вродебы можно небезопасно публиковать. В принципе, можно и мутабл класс написать готовым для unsafe публикации, но это довольно непросто. Но техники есть. Скажем, прокинуть volatile поля в контейнер и добавить его через final reference.

2. Сомневаюсь, что синтетические геттеры повлияют на производительность. Jit должен заинлайнить их по идее, как и обычные. Но мб авторы эклипса что-то такое знают.
>> Вы полагаете, что в каком-нибудь потоке будет ссылка на не до конца проинициализированный экземпляр целевого класса?

Модель памяти JVM позволяет подобное — т.е. сначала выделить память под объект, потом присвоить указатель на нее в поле, и только потом вызвать конструктор. Именно поэтому даже для double checked locking нужен volatile (причем с семантикой Java 5+).
Я не очень представляю, где тут может помочь volatile. Предположим, поле accessor я сделал volatile. Что изменится? Доступ к экземпляру целевого класса у меня осуществляется через промежуточное поле с модификатором final, а его корректное значение вроде бы гарантируется.
Но при этом, насколько я понимаю, не гарантируется целостность самого этого экземпляра. Во время создания первым потоком экземпляра второй поток может прочитать уже обновленный accessor (он скорее всего опубликован безопасно, да), а через него добраться до самого экземпляра, который будет ещё находиться в процессе конструирования (потому что о безопасности его публикации мы, в общем-то, ничего сказать уже не можем).

Volatile accessor, скорее всего, спасёт ситуацию, определив happens-before между первым и вторым потоком. Если мыслить в терминах барьеров, а не спецификации, запись в volatile имеет release семантику, что означает, что никакие операции записи (например, инициализация полей нашего экземпляра при конструировании) не «утекут» вниз через барьер, который ставит запись volatile accessor'а. У чтения volatile acquire семантика, так что со вторым потоком тоже должно быть всё в порядке.

P.S. Не джавист, мог налажать (и скорее всего ...). JMM — штука непростая для понимания, и я бы, наигравшись в «ещё одну» реализацию синглтона из академического интереса, выкинул это всё к чертям, и использовал старый добрый synchronized, ну, может быть с volatile double check locking. :)
Вы все правильно написали. Если volatile нет, то запись в поле ссылки на неинициализированный объект валидна, потому что данный поток её не может увидеть (т.к. следом сразу же зовется конструктор), а другие потоки значения не имеют, т.к. им никаких гарантий и не подразумевается. Если volatile есть, то (в Java 5 и выше), VM должен предусмотреть одновременное чтение с разных потоков — а стало быть, записать ссылку только после полной инициализации объекта.
Речь идет о доступе к accessor — он у вас не final и не volatile. Соответственно, один из потоков может получить DummyInstance, у которого instance==null (т.к. конструктор еще тупо не успел выполниться).
По-моему, тут вы ошибаетесь, всё же тут есть freeze action после выполнения конструктора, который сериализует инициализацию поля instance и публикацию accessor'а. Проблема в публикации самого экземпляра через поле instance.
>> По-моему, тут вы ошибаетесь, всё же тут есть freeze action после выполнения конструктора, который сериализует инициализацию поля instance и публикацию accessor'а.

Только с точки зрения того потока, который позвал конструктор. Для остальных (если они не используют барьеры) никаких гарантий нет.

>> Проблема в публикации самого экземпляра через поле instance.

Поле instance для данного конкретного экземпляра записывается только один раз (причем под synchronized), так что я не вижу тут проблем. Единственный способ прочитать там что-то не то — это позвать get на неинициализированном экземпляре DummyInstance.
Для остальных (если они не используют барьеры) никаких гарантий нет.

Ну, как так нет? А нафиг он тогда иначе нужен? :) Внутри одного потока у нас и так гарантируется sequential consistency.
И насколько я понимаю, реализуется freeze action через те же самые барьеры.

Единственный способ прочитать там что-то не то — это позвать get на неинициализированном экземпляре DummyInstance.

Если не использовать небезопасную публикацию this изнутри конструктора DummyInstance, то у вас никак не получится добраться до неинициализированного (в части final полей) объекта из другого потока. В этом суть freeze action.

конструктор еще тупо не успел выполниться

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

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

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

Проблема в том, что второму потоку пофиг на этот synchronized, он до него не доходит, потому что идет в обход через DummyInstance, минуя все барьеры, необходимые для чтения консистентного содержимого целевого экземпляра. Барьеры должны идти парами, если в одном потоке есть барьер на запись, а в другом нет барьера на чтение, то смысла в первом барьере нет.
>> Если не использовать небезопасную публикацию this изнутри конструктора DummyInstance, то у вас никак не получится добраться до неинициализированного (в части final полей) объекта из другого потока. В этом суть freeze action.

Долго думал, все равно не понял. Поясните на пальцах, каким образом можно получить null через get(), если тот поток, который его дергает, не будет видеть недоинициализированный DummyInstance (причем не важно, почему он недоинициализировался «на самом деле»- с т.з. этого потока с равным успехом можно считать, что конструктор «еще не выполнился», т.к. его единственный эффект — это инициализация поля)?

Если он видит что-то другое, то это либо инициализированный DummyInstance (и тогда все ок), либо SynchrnoizedFunction (и тогда уже возьмется лок, и вторая проверка будет под ним — и он увидит синглтон, если его уже создал другой поток).
С DummyInstance проблем никаких, получить null через get() не получится. Но через get() можно получить объект, конструктор которого еще не выполнен (точнее сказать, результат его выполнения не закоммичен или не виден этому потоку).

То есть если добавить к классу Singleton не-final полей, то их инициализация не гарантируется.

public final class Singleton {
    private int answer = 42;
    ...
}

// thread 1:
s1 = Singleton.getInstance();  // creates a new intance
assert s1.answer == 42;        // works fine: sequential consistency

// thread 2:
s2 = Singleton.getInstance();  // returns an instance created by thread 1
assert s2.answer == 42;        // may fail: s2.answer can be 0 (the default for int)


То есть DummyInstance инициализирован, но объект, на который ссылается его final instance, ещё нет.

Надеюсь, сумел объяснить.
Хм. Теперь я что-то сам уже засомневался в своих словах.

Отсюда:
Java Concurrency in Practice mentions this in section 16.3:

Initialization safety guarantees that for properly constructed objects, all threads will see the correct values of final fields that were set by the constructor, regardless of how the object is published. Further, any variables that can be reached through a final field of a properly constructed object (such as the elements of a final array or the contents of a HashMap referenced by a final field) are also guaranteed to be visible to other threads. For objects with final fields, initialization safety prohibits reordering any part of construction with the initial load of a reference to that object. All writes to final fields made by the constructor, as well as to any variables reachable through those fields, become “frozen” when the constructor completes, and any thread that obtains a reference to that object is guaranteed to see a value that is at least as up to date as the frozen value. Writes that initialize variables reachable through final fields are not reordered with operations following the post-construction freeze.

Похоже, я неправ. Final даёт больше гарантий, чем я думал. Извините, что ввёл в заблуждение.
Спасибо за цитату. Я это тоже где-то читал, поэтому везде полагался на безопасность final-полей.
Напишу, что меня сбило с толку в своих собственных рассуждениях.

Проблема в том, что второму потоку пофиг на этот synchronized, он до него не доходит, потому что идет в обход через DummyInstance, минуя все барьеры, необходимые для чтения консистентного содержимого целевого экземпляра. Барьеры должны идти парами, если в одном потоке есть барьер на запись, а в другом нет барьера на чтение, то смысла в первом барьере нет.

Я исходил из того, что без барьера на чтение во втором потоке код будет заведомо неправильным. Но я не учёл, что если две операции чтения зависимы между собой, то на большинстве архитектур (кроме DEC Alpha) для сериализации таких операций чтения между собой явный барьер не требуется.

В данном случае зависимые операции — это чтение ссылки на accessor и чтение (разыменование) final поля этого объекта. Иными словами, барьеры здесь так или иначе всё-равно идут парой: в первом потоке барьером на запись является freeze action, во втором — неявный барьер чтения через data dependency.

В The JSR-133 Cookbook for Compiler Writers есть неплохое объяснение того, как это всё устроено под капотом (спасибо Fomka за ссылку):
Memory Barriers

Plus the special final-field rule requiring a StoreStore barrier in
x.finalField = v; StoreStore; sharedRef = x;
Data Dependency and Barriers
The need for LoadLoad and LoadStore barriers on some processors interacts with their ordering guarantees for dependent instructions. On some (most) processors, a load or store that is dependent on the value of a previous load are ordered by the processor without need for an explicit barrier. This commonly arises in two kinds of cases, indirection:
Load x; Load x.field
and control
Load x; if (predicate(x)) Load or Store y;
Processors that do NOT respect indirection ordering in particular require barriers for final field access for references initially obtained through shared references:
x = sharedRef; ... ; LoadLoad; i = x.finalField;
Conversely, as discussed below, processors that DO respect data dependencies provide several opportunities to optimize away LoadLoad and LoadStore barrier instructions that would otherwise need to be issued. (However, dependency does NOT automatically remove the need for StoreLoad barriers on any processor.)
Если accessor сделать volatile, то JMM гарантирует вам happens-before между чтением значения и записью значения. А из этого следует happens-before между вызовами метода объекта и его конструктором. Как раз отсутствие этого happens-before и приводит к разным спецэффектам.
Дальше еще ньюанс, даже если конструктор уже закончил инициализацию объекта, то поток, который получил ссылку на этот объект небезопасно может увидеть его побитым. JMM дает гарантии только на final поля, вся остальная грамотная инициализация на совести пользователя. Там еще веселее на самом деле, даже увидев грамотно сконструированный объект, нельзя гарантировать, что в следующем чтении он не станет побитым назад. То есть произойдет откат по времени, JMM допускает такое. Более того, оно так и происходит на некоторых экзотических платформах. Чтение из одной и той же переменной может в любой момент прочитать то, что туда была записано без happens-before, даже если это было далеко в прошлом. Это неочевидно.

Теперь, еще веселее. Если даже мы пометим все методы класса как synchronized, потом весь конструктор положим в synchronized (this) {} и никогда не будем инициализировать непосредственно поля. Даже это не защищает, как я понимаю, объект от вызова методов ДО конструктора. Более того, как я понимаю, мы вообще никак не можем защититься от вызова методов до конструктора ВНУТРИ объекта. Только логикой публикации снаружи. Исключение, это инициализация через final поля и их гарантии пресловутые.

Пример
public static Obj obj;

obj = new Obj();
Если другой объект прочитает этот obj, то у него нет гарантий, что конструктор вообще вызвался. Зато у него есть гарантия, что все его final поля проинициализированы. Но, что особо неочевидно, у него нет гарантий, что volatile поля проинициализированы.
Более того, если другой поток вызовет у объекта метод первый раз он может увидеть хороший объект, проинициализированный. А вот в следующее чтение он его же получит побитым, даже если никакой другой поток этот объект больше не трогал.

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


Я, пожалуй, с вами соглашусь. Лучше сделать код понадежнее, чем потом отлавливать неочевидные эффекты многопоточности.
Странно, что ещё никто не поправил. Дефолтный Эклипс подобных предупреждений не выводит и никаких порочных практик не навязывает. У автора, видимо, стоит какой-то плагин типа Style Check, который и добавляет предупреждение. А в плагине стоит какой-то набор правил, в котором есть вот такое спорное (или ложно срабатывающее) правило. В любом случае, Эклипс тут совершенно не при чём.
Вроде, все по дефолту. Недавно поставил себе Eclipse Kepler Service Release 2.
А сборка какая? Вот специально проверил — загрузил ваш код в Eclipse Luna for JEE Developers. И получил чистое окно кода — никаких ошибко, предупреждений и вообще сообщений. Посмотрите в Help -> About Eclipse -> Installation Details. Я уверен это какое-то расширение добавляет сообщение.
— Громоздкий код
— 3 дополнительных класса: интерфейс и 2 реализации


Далеко не минусы, если использовать например Lombok. Нужно написать один XHandler и весь код будет создаватся во время компиляции. Пример работы можно увидеть тут например: github.com/consulo/consulo/wiki/Annotations
Объясните ламеру, что такое DI? Гугль мне подсказывает, что это Digitally Imported, но подозреваю, что в комментах выше имелся в виду не музыкальный стиль?
Sign up to leave a comment.

Articles