Безопасная публикация и инициализация Java-объектов, или #когдаужепочинятdoublecheckedlocking

JAVA*
Пост из серии «будни перформанс-инженеров» и «JavaOne круглый год».

К моему величайшему facepalm'у на прошедшем JavaOne была тьма вопросов про double-checked locking, и как правильно делать синглетоны. На большую часть этих вопросов уже ответил Walrus, а здесь я хочу подытожить. Надеюсь этим постом раз и навсегда поставить точку в разговорах про double-checked locking и синглетоны. А то мне придётся сделать резиновую печать с URL этого поста и ставить её спрашивающим на лоб.

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

I. Теоретическая подготовка: фабрики и безопасная публикация


Меня немножко возмущает, когда смешивают понятие собственно синглетона и фабрики синглетонов. Для целей нашего поста эти две сущности нам надо будет друг от друга отличать. Всё описаное, понятно, также распространяется на синглетон, в который фабрика уже внедрена (то есть существует метод static getInstance()).

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

Понятно, что вот такое:

public class SynchronizedFactory {
    private Singleton instance;

    public Singleton get() {
        synchronized(this) {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
}

… удовлетворяет требованиям 1 и 2, но не удовлетворяет требованию 3.

На этом месте рождается идиома Double-Checked Locking. Она берёт своё начало из идеи, что нечего лишний раз синхронизироваться, если подавляющее количество вызовов уже обнаружит синглетон инициализированным. Поэтому разные люди берут и пишут:

public class NonVolatileDCLFactory {
    private Singleton instance;

    public Singleton get() {
        if (instance == null) {  // check 1
            synchronized(this) {
                if (instance == null) { // check 2
                    instance = new Singleton();
                }
            }
        }
        return instance;
     }
}

К сожалению, эта хрень не всегда работает корректно. Казалось бы, если проверка check 1 не выполнилась, то instance уже инициализирован и его можно возвращать. А вот и нет! Он инициализирован с точки зрения потока, который произвёл изначальное присвоение! Нет никаких гарантий, что вы обнаружите в полях синглетона то, что вы записали внутри его конструктора, если будете читать в другом потоке.

Здесь можно было бы начать объяснять про happens-before, но это довольно тяжёлый формализм. Вместо этого мы будем использовать феноменологическое объяснение, в виде понятия безопасной публикации. Безопасная публикация обеспечивает видимость всех значений, записанных до публикации, всем последующим читателям. Элементарных способов безопасной публикации несколько:
  • инициализация статическим инициализатором (JLS 12.4)
  • запись в поле, корректно защищённое локом (JLS 17.4.5)
  • запись в volatile-поле (JLS 17.4.5), и как следствие, запись в атомики
  • запись в final-поле (JLS 17.5)

Обратим внимание, что в NonVolatileDCL поле $instance…
  • не инициализируется статикой
  • не защищено локом как минимум одно чтение
  • не записывается в volatile
  • не записывается в final

То есть, по определению, публикация $instance в NonVolatileDCL безопасной не является. Смотрите, кстати, сколько из этого следует забавных возможностей для безопасной фабрики синглетонов. Начиная с уже навязшего в зубах:

public class VolatileDCLFactory {
    private volatile Singleton instance;

    public Singleton get() {
        if (instance == null) {  // check 1
            synchronized(this) {
                if (instance == null) { // check 2
                    instance = new Singleton();
                }
            }
        }
        return instance;
     }
}

… продолжая не менее классическим holder idiom, который безопасно публикует, записывая объект статическим инициализатором:

public class HolderFactory {
    public static Singleton get() {
        return Holder.instance;
    }

    private static class Holder {
        public static final Singleton instance = new Singleton();
    }
}

… и заканчивая final-полем. Поскольку в final-поле вне конструктора писать уже поздно, нужно сделать:

public class FinalWrapperFactory {
    private FinalWrapper wrapper;

    public Singleton get() {
        if (wrapper == null) {  // check 1
            synchronized(this) {
                if (wrapper == null) { // check 2
                    wrapper = new FinalWrapper(new Singleton()); 
                }
            }
        }
        return wrapper.instance;
     }

     private static class FinalWrapper {
        public final Singleton instance;
        public FinalWrapper(Singleton instance) {
            this.instance = instance;
    }
}


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

Кроме того, в наш зачёт с криком «одна бабка мне сказала, что volatile это дорого!» врывается новый кандидат, кеширующий поле в локале:

public class VolatileCacheDCLFactory implements Factory {
    private volatile Singleton instance;

    @Override
    public Singleton getInstance() {
        Singleton res = instance;
        if (res == null) {
            synchronized (this) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
            return instance;
        }
        return res;
    }
}

II. Теоретическая подготовка: синглетоны и безопасная инициализация


Идём дальше. Объект можно сделать всегда безопасным для публикации. JMM гарантирует видимость всех final-полей после завершения конструктора. Вот пример полностью безопасной инициализации:

public class SafeSingleton implements Singleton {
    private final Object obj1;
    private final Object obj2;
    private final Object obj3;
    private final Object obj4;

    public SafeSingleton() {
        obj1 = new Object();
        obj2 = new Object();
        obj3 = new Object();
        obj4 = new Object();
    } 
    ...
}

Замечу, что в некоторых случаях это распространяется не только на final поля, но и на volatile. Есть ещё более фимозные техники, типа synchronized в конструкторе, можете почитать у cheremin(поделитесь с Русланом инвайтом ;)), он такое любит. В этом посте таких высоких материй мы касаться не будем.

Вот такой объект, понятно, будет небезопасным:

public final class UnsafeSingleton implements Singleton {

    private Object obj1;
    private Object obj2;
    private Object obj3;
    private Object obj4;

    public UnsafeSingleton() {
        obj1 = new Object();
        obj2 = new Object();
        obj3 = new Object();
        obj4 = new Object();
    }
  
   ...
}

На самом деле, проблемы с небезопасно опубликованным небезопасным синглетоном скажутся в некоторых специальных граничных условиях, например, если конструктор синглетона заинлайнится в getInstance() фабрики Тогда ссылка на недоконструированный объект может быть присвоена в $instance до фактического завершения конструктора.

Вот, например, хвост NonVolatileDCLFactory.getInstance() для UnsafeSingleton (конструктор синглетона заинлайнился):
178     MEMBAR-storestore (empty encoding)
178     #checkcastPP of EAX
178     MOV    [ESI + #8],EBX ! Field net/shipilev/singleton/NonVolatileDCLFactory.instance
17b     MOV    [EDI + #20],EAX ! Field net/shipilev/singleton/UnsafeSingleton.obj4
17e     MOV    ECX, ESI # CastP2X
180     MOV    EBP, EDI # CastP2X
182     SHR    ECX,#9
185     SHR    EBP,#9
188     MOV8   [EBP + 0x6eb16a80],#0
18f     MOV8   [ECX + 0x6eb16a80],#0
18f
196   B16: #    B32 B17 <- B15 B4  Freq: 0.263953
196     MEMBAR-release (a FastUnlock follows so empty encoding)
196     MOV    ECX,#7
19b     AND    ECX,[ESI]
19d     CMP    ECX,#5
1a0     Jne    B32  P=0.000001 C=-1.000000
1a0
1a6   B17: #    B18 <- B33 B32 B16  Freq: 0.263953
1a6     MOV    EAX,[ESI + #8] ! Field net/shipilev/singleton/NonVolatileDCLFactory.instance
1a6
1a9   B18: #    N523 <- B17 B1  Freq: 1
1a9     ADD    ESP,24   # Destroy frame
        POPL   EBP
        TEST   PollPage,EAX     ! Poll Safepoint

1b3     RET

Обратите внимание на присвоение $instance до присвоения $obj4.

А вот тот же самый NonVolatileDCLFactory с SafeSingleton:

178     MEMBAR-storestore (empty encoding)
178     #checkcastPP of EAX
178     MOV    [EDI + #20],EAX ! Field net/shipilev/singleton/SafeSingleton.obj4
17b     MOV    ECX, EDI # CastP2X
17d     SHR    ECX,#9
180     MOV8   [ECX + 0x6eb66800],#0
187     MEMBAR-release ! (empty encoding)
187     MOV    [ESI + #8],EBX ! Field net/shipilev/singleton/NonVolatileDCLFactory.instance
18a     MOV    ECX, ESI # CastP2X
18c     SHR    ECX,#9
18f     MOV8   [ECX + 0x6eb66800],#0
18f
196   B16: #    B32 B17 <- B15 B4  Freq: 0.24361
196     MEMBAR-release (a FastUnlock follows so empty encoding)
196     MOV    ECX,#7
19b     AND    ECX,[ESI]
19d     CMP    ECX,#5
1a0     Jne    B32  P=0.000001 C=-1.000000
1a0
1a6   B17: #    B18 <- B33 B32 B16  Freq: 0.24361
1a6     MOV    EAX,[ESI + #8] ! Field net/shipilev/singleton/NonVolatileDCLFactory.instance
1a6
1a9   B18: #    N524 <- B17 B1  Freq: 1
1a9     ADD    ESP,24   # Destroy frame
        POPL   EBP
        TEST   PollPage,EAX     ! Poll Safepoint

1b3     RET

Видно, что $instance пишется после всех полей.

Для тех, кто не запарился до сюда дочитать, небольшой бонус. HotSpot следует консервативной рекомендации из JSR-133 Cookbook: «Issue a StoreStore barrier after all stores but before return from any constructor for any class with a final field.»

Другими словами, есть специфичная для хотспота фишка:

    ...
    // This method (which must be a constructor by the rules of Java)
    // wrote a final.  The effects of all initializations must be
    // committed to memory before any code after the constructor
    // publishes the reference to the newly constructor object.
    // Rather than wait for the publication, we simply block the
    // writes here.  Rather than put a barrier on only those writes
    // which are required to complete, we force all writes to complete.
    ...

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

public class TrickySingleton implements Singleton {
    private final Object barrier;
    private Object obj1;
    private Object obj2;
    private Object obj3;
    private Object obj4;

    public TrickySingleton() {
        barrier = new Object();
        obj1 = new Object();
        obj2 = new Object();
        obj3 = new Object();
        obj4 = new Object();
    }
    ...
}

… и это будет эффективно безопасной публикацией, но только на хотспоте. При этом нет особенной разницы, в каком порядке пишутся поля (но только пока *devil_laugh*).

Это несколько умозрительный случай, но внимательный читатель оценит симпатичные грабли: жил-был класс с кучей нефинальных полей и одним финальным. Тесты проходят, приложение работает, объект как будто безопасно публикуется. Потом приходит Вова и рефакторит класс, удаляя финальное поле — и всё, кранты безопасной публикации. Вова смотрит в свой коммит и не понимает, как такое возможно.

Итого, у нас есть шесть вариантов фабрик и три синглетона.

III. Ломаем DCL


Когда-то давно gvsmirnov меня спрашивал, можно ли действительно продемонстрировать такой реордеринг, который сломает DCL. Как видно из ассемблера вверху, гадкие реордеринги даже в присутствии Total Store Order'а нам может преподнести компилятор. Почему он это сделает, тайна сия велика есть, ему никто не запрещал.

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

    private volatile Factory factory;
    private volatile boolean stopped;    

    public class Runner implements Runnable {

        public void run() {
            long iterations = 0;
            long selfCheckFailed = 0;

            while (!stopped) {
                Singleton singleton = factory.getInstance();
                if (singleton == null || !singleton.selfCheck()) {
                    selfCheckFailed++;
                }
                iterations++;

                // switch to new factory
                factory = FactorySelector.newFactory();
            }

            pw.printf("%d of %d had failed (%e)\n", selfCheckFailed, iterations, 1.0D * selfCheckFailed / iterations);
        }
    }

Полный проект лежит вот тут, можете поиграться. -DfactoryType, -DsingletonType выбирают фабрику и синглетон, -Dthreads регулирует количество потоков, а -Dtime — время на тест.

Синглетон проверяет свои поля методом:

    public boolean selfCheck() {
        return  (obj1 != null) &&
                (obj2 != null) &&
                (obj3 != null) &&
                (obj4 != null);
    }

… то есть по сути смотрит, были ли таки инициализированы поля у того инстанса, который отдала фабрика.

Ну что, посчитаем вероятности отказа. Гоняем тесты по 10 минут: за это время миллиарды новых синглетонов успевают создаваться, сталкиваться, разлетаться на фермионы, бозоны… чёрт, кажется, я не туда пишу. Никаким таким тестом доказать корректность многопоточного кода нельзя, тестом её можно только опровергнуть.

На приличных размеров Nehalem'е (2 sockets, 6 cores per socket, 2 strands per core = 24 hw threads), JDK 7u4 x86_64, RHEL 5.5, -Xmx8g -Xms8g -XX:+UseNUMA -XX:+UseCompressedOops, в 24 потоках; метрика — вероятность отказа:
Unsafe Safe Tricky
Synchronized ε ε ε
NonVolatileDCL 3*10-4 ε ε
VolatileDCL ε ε ε
VolatileCacheDCL ε ε ε
Holder N/A N/A N/A
FinalWrapperDCL ε ε ε
ε < 10-11, т.е. ни одного фейла не произошло, но это не значит, что их никогда не будет :)

Что мы видим?
  • Некорректно сконструированный синглетон (Unsafe) нормально работает с корректно публикующими фабриками
  • Некорректно публикующая фабрика (NonVolatileDCL) нормально работает с корректно сконструированными синглетонами
  • Когда эти двое встречаются, начинается треш и угар, причём с приличной вероятностью отказа: фейлом оканчивается 1 вызов из 3000
  • Holder дисквалифицирован, т.к. сохраняет своё состояние в статике

Дабы меня не обвинили в великодержавном шовинизме, вот тот же тест на двухядерном NVidia Tegra2 (Cortex A9) и JDK 7u4 (ARM port), -Xmx512m -Xms512m -XX:+UseNUMA в двух потоках; метрика — вероятность отказа:
Unsafe Safe Tricky
Synchronized ε ε ε
NonVolatileDCL 2*10-8 ε ε
VolatileDCL ε ε ε
VolatileCacheDCL ε ε ε
Holder N/A N/A N/A
FinalWrapperDCL ε ε ε
ε < 10-10, т.е. ни одного фейла не произошло, но это не значит, что они не появятся в будущем. ε существенно меньше, потому что ARM медленее, а тест выполняется те же 10 минут.

Что мы видим? Да тоже самое и видим. Несмотря на то, что x86 и ARM — очень разные платформы с точки зрения модели памяти, гарантированное поведение остаётся гарантированным. Вероятность отказа сильно упала ввиду специфики теста: глобальный эффект от безопасной публикации самой factory частично сглаживает эффекты от теста.

IV. Performance


Написать корректный параллельный код — дело не хитрое. Оберни всё глобальным локом, и вперёд. Проблема написать корректный и эффективный параллельный код. Ввиду того, что на J1 мне умудрялись говорить «ой, volatile в DCL это так дорого, мы лучше синхронизуем getInstance()», придётся наглядно показать, что к чему. Не буду показывать много графиков, покажу только пару точек с тех же платформах, где гонялась корректность.

Очень простой микробенчмарк в нашем внутреннем тёплом ламповом харнессе выглядит так:
public class SteadyBench { // все инстансы SteadyBench шарятся между потоками
    private Factory factory;

    @Setup
    public void setUp() {
        factory = FactorySelector.newFactory();
    }

    @TearDown
    public void teardown() {
        factory = null;
    }

    @GenerateMicroBenchmark(share = Options.Tristate.TRUE)  
    public Object test() {  // этот метод зовётся в цикле много-много раз
        return factory.getInstance(); 
    }
}

Поскольку наш харнесс ещё не открыт, вам придётся немножко поработать, чтобы написать полный микробенчмарк.

Брать синглетон у уже горячей фабрики — подавляющий use case в продакшене. Замечу, что микротест, который сильно амплифицирует стоимость даже элементарных операций, т.е. если что-то в этом тесте быстрее в два раза, то это не значит, что большой проект тоже разгонится в два раза с «правильной идиомой». Хотя бывает, особенно для локов.

x86, Nehalem, 24 hardware threads; метрика: миллионы операций в секунду, чем больше, тем лучше:
1 thread 24 threads
Unsafe Safe Tricky Unsafe Safe Tricky
Synchronized 46 ± 2 47 ± 2 43 ± 2 9 ± 2 25 ± 2 22 ± 3
NonVolatileDCL 386 ± 23 473 ± 2 463 ± 5 5103 ± 131 4955 ± 125 4981 ± 136
VolatileDCL 394 ± 15 405 ± 3 402 ± 13 3977 ± 89 4576 ± 101 4620 ± 92
VolatileCachedDCL 454 ± 8 465 ± 3 460 ± 6 4778 ± 250 4946 ± 143 5071 ± 113
Holder 554 ± 3 520 ± 7 540 ± 5 6125 ± 124 6131 ± 102 6114 ± 116
FinalWrapperDCL 415 ± 12 390 ± 10 359 ± 6 4566 ± 114 4585 ± 102 4231 ± 106

Что мы здесь видим?
  • про Synchronized даже говорить нечего, она раздулась в настоящий лок и там всё очень-очень плохо
  • NonVolatile работает хорошо и непринуждённо
  • Volatile иногда работает похуже, сказывается необходимость читать $instance из памяти два раза, что делает этот вариант чуть медленнее NonVolatile
  • VolatileCache частично нивелирует этот эффект; показывая, что накладных расходов на само volatile-чтение нет
  • FinalWrapper работает так же как Volatile как раз по этой причине: нужно сделать один лишний дереференс, один лишний поход в память, один лишний потенциальный cache miss
  • Holder впереди планеты всей; казалось бы, ну как? Фокус в том, что к моменту компиляции методов этой фабрики HotSpot знает, что сам холдер уже загружен, и ему не нужно делать вообще никаких проверок, а сразу отдать статический $instance


ARMv7, Cortex A9, 2 hardware threads; метрика: миллионы операций в секунду, чем больше, тем лучше:
1 thread 2 threads
Unsafe Safe Tricky Unsafe Safe Tricky
Synchronized 7.1 ± 0.1 7.1 ± 0.1 7.1 ± 0.1 1.9 ± 0.1 1.9 ± 0.1 1.9 ± 0.1
NonVolatileDCL 23.6 ± 0.1 23.6 ± 0.1 23.6 ± 0.1 45.5 ± 1.8 47.0 ± 0.1 47.0 ± 0.1
VolatileDCL 13.4 ± 0.1 13.4 ± 0.1 13.4 ± 0.1 26.6 ± 0.1 26.6 ± 0.1 26.6 ± 0.1
VolatileCachedDCL 17.4 ± 0.1 17.4 ± 0.1 17.4 ± 0.1 34.6 ± 0.1 34.6 ± 0.1 34.6 ± 0.1
Holder 24.2 ± 0.1 24.2 ± 0.1 24.2 ± 0.1 47.8 ± 0.8 47.9 ± 0.8 48.0 ± 0.1
FinalWrapperDCL 24.2 ± 0.1 24.2 ± 0.1 24.2 ± 0.1 48.1 ± 0.1 46.8 ± 2.2 46.8 ± 2.3

Что мы здесь видим?
  • всё более-менее соотносится с x86, кроме того, что...
  • volatile-чтение на ARM'е требует барьера, поэтому VolatileDCL оттормаживает
  • можно сэкономить на стоимости volatile-чтения, скешировав значение в локале, VolatileCacheDCL это и делает; однако полностью избавиться от оверхеда нельзя, до NonVolatile так и не дотянуло

V. Выводы, обобщения и оговорки


Главный вывод запечатлейте у себя: DCL работает!
Оговорка 1: Если безопасно инициализирован, или безопасно опубликован, или и то и другое.
Оговорка 2: Только в Java 5+.

Рецепты:
  1. Не делайте ленивую инициализацию там, где сойдёт неленивая
  2. Нужна статическая ленивая фабрика? Вам в Holder. Её особенно не выгрузишь, но зато спекулятивные оптимизации на вашей стороне
  3. Нужна нестатическая ленивая фабрика? Можете использовать NonVolatileDCL, но только если объект безопасно конструируется
  4. Нужна нестатическая ленивая фабрика, и гарантировать безопасность конструирования нельзя? Используйте Volatile(Cached)DCL или FinalWrapperDCL, в зависимости от того, чем вы хотите пожертвовать — потенциальной стоимостью volatile на ARM'е, или потенциальной стоимостью лишнего дереференса

+92
6 мая 2012 в 18:02
247
TheShade 100,0

комментарии (48)

+15
akira #
Хорошая статья, а то «Пишем первое приложение на Андроид» немного утомили.
–5
vittore #
Странно, что первым коментарием не было «хабр-кондитерское изделие»
–6
sneer #
+6
TheShade #
Вы бы подревнее ещё статью откопали, ага, 2002 год.
Java 5 вышла в 2004-ом, с поправленной моделью памяти, в которой DCL работает.
0
pavelrappo #
Читатель диагональю?
0
NikitOS9 #
да,
очень рад что переходим полностью с 'чистой' java на clojure.
хотел еще на j1 спросить, не устал ли народ от фабрик, синглетонов, гемора с потоками и тд…
+7
TheShade #
Не устал. Даже с Clojure вы же всё равно будете работать на машинах с разделяемой памятью. А значит, рантайму за вас придётся поддерживать консистентность, за которую вы в итоге будете расплачиваться производительностью. И это проблема фундаментальная: чем выше абстракция, тем удобнее решать целевые задачи, и тем больнее решать задачи нецелевые. Решение проблем с производительностью очень часто является нецелевой задачей ;) Вот будет у вас программа на Clojure работать медленно, что вы будете делать? Без понимания того, что происходит «под капотом», перформансом заниматься можно, но глупо.
0
NikitOS9 #
ну это истина которая и так ясна,
пока за пару месяцев отказа от 'низко уровневой' java,
и по сути получается от великого и ужасного OOP, только положительные ощущения.
времени экономиться уйма(не нужно писать фабрики фабрик и звать перфоманс гуру))
как то все работает и не тормозит(дальше посмотрим)

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

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

если именно в этом работа(искать причины торможения чужих программ), как у вас получается, то вопросов нет. но если работа в том чтобы сделать заказ быстро и качественно, то java тут думаю не впереди.
0
TheShade #
Ну да, ну да. Понятно, что на голых платформах мало кто что пишет, все обычно возводят свои горы библиотек, фреймворков, облегчалок и ускорялок. Некоторые даже настолько далеко заходят, что возводят вокруг стены, называют свою поделку языком и начинают её развивать как отдельную платформу. Явление не новое: такой путь прошёл C от ассемблера, Java от C, теперь вот Clojure и иже с ними от Java*. Только вот практика показывает, что требования знать все слои никто не отменяет, потому что когда на текущем слое абстракции возникают проблемы, внезапно оказывается, что «we need to go deeper» (с) и тут-то со знаниями «ёк».

* тут надо отличать Java и JVM
0
olegchir #
Еще можно вместо «go deeper» попробовать придумать другую реализацию решения, чтобы в рамках используемой платформы она работала из коробки…
+6
pavelrappo #
У меня вот случился насморк. Нос перестал нормально функционировать. Казалось бы. Две дырки. Вдох. Выдох. Чего уж проще-то? Однако, это оказалось абстракцией… текущей…

шмыгает носом
0
sneer #
java как платформа зашевелилась. Уже jre7 уже с последним апдейтом рекомендован к употреблению. Летом можно было бы уже попробовать java8 где язык не плохо так прокачали, но перенесли на февраль дев превью. Этот дев превью по зрелости будет как java7 релиз. Они обожглись в этот раз и решили не спешить и обкататать прежде чем говорить о том что это можно юзать.
0
MagicWolf #
А вы — это кто? Вам сотрудник с опытом в Clojure не нужен?
+10
olegchir #
Этот пост станет классикой, поэтому нуждается в следующей картинке:

+9
TheShade #
Ну отлично, давайте превратим пост в фишки.нет, ага.
+1
lightman #
Давайте. Необходимо развеять обывательский миф о том, что IT это скучно.
+1
pavelrappo #
Мне, Лёш, кажется, что в статье для полного понимания «почему оно так» не хватает понятия out of thin air safety.
0
Elfet #
Где и по каким книжкам можно научится Java и всем этим потокобезопасным фишкам?
+1
olegchir #
Про потоки было в Java Concurrency in Practice
+1
NikitOS9 #
Java Concurrency Guidelines
0
B08AH #
К слову — заметил, что коменты в нашем коде есть «i hate singletons»…
+2
gvsmirnov #
Мало того что DCL сломал, так ещё и за-RickRoll-ил всех! :)
+2
malexejev #
Стоило тогда еще ENUM-имплементацию синглтонов рассмотреть, если уж на то пошло.
+2
TheShade #
Со стилистической точки зрения считаю enum singleton обфусцирующим код хаком, с практической точки зрения считаю эквивалентом Holder. Смысла считать его отдельной реализацией не вижу, как бы он не нравился Джошу Блоху.
0
malexejev #
Ну по стилистике понятно что это грязный хак, я такое сам не использую и через ревью не пропущу.

Тем не менее, метод активно рекламируется с подачи уважаемого мужчины, поэтому узнать почему он эквивалентен Holder-у было бы полезно. Я сейчас глянул внутренности java.lang.Enum — там этого ответа нет.
0
TheShade #
Они эквивалентны в том смысле, что механизмы поддержания уникальности значения Enum'а и уникальности значения static'а в Holder'е суть один и тот же механизм. Возьмите javap -c и убедитесь, enum разворачивается самим javac'ом в:

final class net.shipilev.singleton.EnumFactory$Holder extends java.lang.Enum<net.shipilev.singleton.EnumFactory$Holder> {
  public static final net.shipilev.singleton.EnumFactory$Holder INSTANCE;
 
  static {
    // тут инициализация INSTANCE
  };


Уважаемый мужчина (tm) сделал много полезного, но и много косяков. Предложение пользовать enum'ы для синглетонов — один из его косяков. Самое смешное в том, что в последних своих докладах уважаемый мужчина вообще не упоминает о нём, а собственноручно предлагает пользовать holder.
0
malexejev #
Ок, спасибо. Я глянул — оно там в общем случае даже массив статический ставит.
0
halyavin #
А как вы получили ассемблерный код?
0
MagicWolf #
>… гадкие реордеринги даже в присутствии Total Store Order'а нам может преподнести компилятор. Почему он это сделает, тайна сия велика есть,…

Да потому что многоядерный процессор сам вправе сделать реордеринг между memory barriers и прочими фишками синхронизации. Поэтому компилятор ничего нового не добавляет.
0
TheShade #
Компилятор добавляет новое. Добавляет реордеринги, которые могут быть невозможны на хардваре. Например, x86 есть архитектура с TSO — там записи видны в том же порядке, что и происходят в программе. Поэтому в конкретном случае DCL может сломаться только в случае, если компилятор переставит записи собственноручно.
0
MagicWolf #
Так я имею в виду не конкретную архитектуру (x86), а общий случай. Нет же возможности скомпилировать byte code только для x86, правильно?
0
TheShade #
И в общем случае компилятор делает свои годные реордеринги, без оглядки на хардвар. Барьеры вообще говоря инструктируют и компилятор, и хардвар не делать специфические реордеринги в специальных местах.

> Нет же возможности скомпилировать byte code только для x86, правильно?

Не понял. Вот вы запускаете JIT на своей x86-машине, и JIT генерирует код «только для x86». С оптимизациями, характерными для x86. С барьерами, нужными на x86. И т.п.
–2
MagicWolf #
Тормознул немного. Получается, что JIT недостаточно продвинут, чтобы лучше оптимизировать код для x86.
0
TheShade #
Семь пятниц на неделе. JIT как раз достаточно продвинут, чтобы оптимизироваться под целевую платформу; например, не эмиттить инструкции для барьеров, если на целевой платформе они не нужны. Из чего вы вдруг заключили обратное?
0
MagicWolf #
Я к тому, что JIT на x86 мог бы спасать программистов, которые используют UnsafeSingleton, хотя делать этого не обязан.

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

>Добавляет реордеринги, которые могут быть невозможны на хардваре.

Вы утверждаете, что такой реордеринг для UnsafeSingleton, как в статье, не может быть организован самим процессором других архитектур? Ни на Spark, ни на Alpha, ни на Itaniumе?
0
TheShade #
> Я к тому, что JIT на x86 мог бы спасать программистов, которые используют UnsafeSingleton, хотя делать этого не обязан.

Спасать от такого поведения — штука прямо противоположная оптимизации.

> Почему вас удивляет, что компилятор делает реордеринг для общего случая, мне непонятно.

Меня не удивляет. Я только говорил о том, что не могу понять, почему конкретно в этом случае компилятор сделал конкретно так. Что он выиграл, переместив записи? Догадываюсь, конечно, но на 100% не понимаю.

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

На платформах с TSO не может быть. Сюда относятся x86 и SPARC TSO.
0
MagicWolf #
>Я только говорил о том, что не могу понять, почему конкретно в этом случае компилятор сделал конкретно так.

Теперь ясно. ИМХО слишком размыто написали насчет «тайна велика».
+1
gvsmirnov #
Да ладно, там по ссылке же объясняется всё :)
+1
enrupt #
у chermin об этом есть
0
enrupt #
не обратил внимания: в статье та же самая ссылка приводится.
0
man4j #
В случае если мы используем NonVolatileDCLFactory + SafeSingleton то поведение программы корректно. Но если данная фабрика будет использоваться из разных потоков, то разные потоки получат свою копию синглтона, т.к. поле instance в фабрике не final и не volatile, в результате чего другие потоки будут некоторое время видеть null из-за кеширования и продолжать создавать новые синглтоны. Или такого происходить не будет из-за «специфичной для хотспота фишки»? Разъясните пожалуйста.
0
TheShade #
Да будет всё нормально работать: по выходу из synchronized все записи в этом блоке становятся видны всем входящим в synchronized. Поток, не захвативший монитор, действительно может увидеть null, но ему потом придётся захватить монитор и проверить ещё раз.

Ибо сказано в Документации: «An unlock (synchronized block or method exit) of a monitor happens-before every subsequent lock (synchronized block or method entry) of that same monitor. And because the happens-before relation is transitive, all actions of a thread prior to unlocking happen-before all actions subsequent to any thread locking that monitor».
0
Alex42rus #
написано однажды — работает везде (с) Java
* — только если вы используете HotSpot
** — только если мы не меняли в HotSpot обработку конструкторов
+1
TheShade #
Пишете по стандарту — работает везде.
Используете специфичные для хотспота фишки — ССЗБ.
Это вы, наверное, ещё про Unsafe не знаете…
+1
pavelrappo #
Вкатил бы я с удовольствием вам минус, только Заратустра карма не позволяет.
0
Alex42rus #
Я не пытаюсь кинуть палку в сторону Java (сам на ней пишу), на правах иронии.

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

Взгляд изнутри: матрицы цифровых камер
Заблуждения программистов относительно времени
Ремонтируем монитор содой!