Яндекс.Метрика

Этот пост сохранён в исторических целях. Его содержание и форматирование не поддерживаются ни в каком виде. Читайте апдейт: "Safe Publication and Safe Initialization in Java"

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

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

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

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

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

Хорошая фабрика синглетонов обладает следующими свойствами:

  • Хорошая фабрика потокобезопасна. Вне зависимости от порядка обращения из разных потоков все они получат один и тот же синглетон. Более того, синглетон будет корректно проинициализирован.

  • Хорошая фабрика ленива (тут можно поспорить, но неленивая фабрика нам здесь неинтересна). Инициализация синглетона происходит при первом запросе на синглетон, а не при загрузке класса синглетона.

  • Хорошая фабрика эффективна, т.е. вносит минимум накладных расходов.

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

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() {
        Singleton local = instance;
        if (local == null) {  // check 1
            synchronized(this) {
                local = instance;
                if (local == null) { // check 2
                    local = new Singleton();
                    instance = local;
                }
            }
        }
        return local;
     }
}

(UPD 2014/02/10: Ниже по тексту в FinalWrapperFactory были две гонки на чтение поля instance, из-за чего первое чтение имеет право прочитать не null, а второе имеет полное право прочитать null и вернуть этот null из метода, и исправил. Год спустя мне <hh user="elizarov"/> подсказал посмотреть и на другие места: здесь такая же проблема исправлена аналогичным образом — мы поле читаем в локальную переменную (с одной гонкой), после чего уже проверяем и возвращаем результат из локальной переменной, а на локальной переменной гонок быть не может в принципе, и если мы проверили что она не null, то и результат обязан быть не null).

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

Здесь можно было бы <a href="http://habrahabr.ru/post/133981/">начать объяснять</a> про happens-before, но это довольно тяжёлый формализм. Вместо этого мы будем использовать феноменологическое объяснение, в виде понятия <i>безопасной публикации</i>. Безопасная публикация обеспечивает видимость всех значений, записанных до публикации, всем последующим читателям. Элементарных способов безопасной публикации несколько:

  • инициализация статическим инициализатором (<a href="http://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#jls-12.4">JLS 12.4</a>)

  • запись в поле, корректно защищённое локом (<a href="http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.5">JLS 17.4.5</a>)

  • запись в volatile-поле (<a href=" http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.5">JLS 17.4.5</a>), и <a href="http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/atomic/package-summary.html">как следствие</a>, запись в атомики

  • запись в final-поле (<a href="http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5">JLS 17.5</a>)

Обратим внимание, что в 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() {
            FinalWrapper w = wrapper;
            if (w == null) { // check 1
                synchronized(this) {
                    w = wrapper;
                    if (w == null) { // check2
                        w = new FinalWrapper(new Singleton());
                        wrapper = w;
                    }
                }
            }
            return w.instance;
     }

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

(UPD: 2013/02/15: Спустя год я понял, что в прошлой версии был потенциальный NPE, проапдейтил)

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

Кроме того, в наш зачёт с криком "одна бабка мне сказала, что 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 в конструкторе, <a href="http://cheremin.blogspot.com/2012/05/unsafe-publication.html">можете почитать</a> у <hh user="cheremin"/><sub>(поделитесь с Русланом инвайтом ;))</sub>, он такое любит. В этом посте таких высоких материй мы касаться не будем.

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

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 следует консервативной рекомендации из <a href="http://g.oswego.edu/dl/jmm/cookbook.html">JSR-133 Cookbook</a>: "Issue a StoreStore barrier after all stores but before return from any constructor for any class with a final field."

Другими словами, есть специфичная для хотспота <a href="http://hg.openjdk.java.net/jdk7u/jdk7u/hotspot/file/tip/src/share/vm/opto/parse1.cpp">фишка</a>:

    ...
    // 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 поле, то он тупо выставляет барьер в конец конструктора и таким образом обеспечивает запись <i>всех</i> полей в конструкторе до записи ссылки на сконструированный объект. Это имеет смысл, чтобы не делать несколько барьеров для нескольких финальных полей. То есть, только для хотспота можно сделать так:

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

Когда-то давно <hh user="gvsmirnov"/> меня спрашивал, можно ли действительно продемонстрировать такой реордеринг, который сломает DCL. Как видно из ассемблера вверху, гадкие реордеринги даже в присутствии Total Store Order’а нам может преподнести компилятор. Почему он это сделает, <a href="http://tinyurl.com/2g9mqh">тайна сия велика</a> есть, ему никто не запрещал.

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

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

Полный проект лежит <a href="http://shipilev.net/pub/articles/dcl-habr/singleton.tar.gz">вот тут</a>, можете поиграться. -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 потоках; метрика — вероятность отказа: <table> <tr> <td></td><td>Unsafe</td><td>Safe</td><td>Tricky</td> </tr> <tr><td>Synchronized</td><td>ε</td><td>ε</td><td>ε</td></tr> <tr><td>NonVolatileDCL</td><td><b>3*10<sup>-4</sup></b></td><td>ε</td><td>ε</td></tr> <tr><td>VolatileDCL</td><td>ε</td><td>ε</td><td>ε</td></tr> <tr><td>VolatileCacheDCL</td><td>ε</td><td>ε</td><td>ε</td></tr> <tr><td>Holder</td><td>N/A</td><td>N/A</td><td>N/A</td></tr> <tr><td>FinalWrapperDCL</td><td>ε</td><td>ε</td><td>ε</td></tr> </table>ε < 10<sup>-11</sup>, т.е. ни одного фейла не произошло, но это не значит, что их никогда не будет :)

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

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

IV. Performance

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

Очень простой микробенчмарк в нашем внутреннем тёплом ламповом харнессе выглядит так:

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; метрика: миллионы операций в секунду, чем больше, тем лучше: <table> <tr> <td></td> <td colspan=3>1 thread</td> <td colspan=3>24 threads</td> </tr> <tr> <td></td><td>Unsafe</td><td>Safe</td><td>Tricky</td><td>Unsafe</td><td>Safe</td><td>Tricky</td> </tr> <tr> <td>Synchronized</td> <td><b>46</b> ± 2</td> <td><b>47</b> ± 2</td> <td><b>43</b> ± 2</td> <td><b>9</b> ± 2</td> <td><b>25</b> ± 2</td> <td><b>22</b> ± 3</td> </tr>

<tr> <td>NonVolatileDCL</td> <td><b>386</b> ± 23</td> <td><b>473</b> ± 2</td> <td><b>463</b> ± 5</td> <td><b>5103</b> ± 131</td> <td><b>4955</b> ± 125</td> <td><b>4981</b> ± 136</td> </tr>

<tr> <td>VolatileDCL</td> <td><b>394</b> ± 15</td> <td><b>405</b> ± 3</td> <td><b>402</b> ± 13</td> <td><b>3977</b> ± 89</td> <td><b>4576</b> ± 101</td> <td><b>4620</b> ± 92</td> </tr>

<tr> <td>VolatileCachedDCL</td> <td><b>454</b> ± 8</td> <td><b>465</b> ± 3</td> <td><b>460</b> ± 6</td> <td><b>4778</b> ± 250</td> <td><b>4946</b> ± 143</td> <td><b>5071</b> ± 113</td> </tr>

<tr> <td>Holder</td> <td><b>554</b> ± 3</td> <td><b>520</b> ± 7</td> <td><b>540</b> ± 5</td> <td><b>6125</b> ± 124</td> <td><b>6131</b> ± 102</td> <td><b>6114</b> ± 116</td> </tr>

<tr> <td>FinalWrapperDCL</td> <td><b>415</b> ± 12</td> <td><b>390</b> ± 10</td> <td><b>359</b> ± 6</td> <td><b>4566</b> ± 114</td> <td><b>4585</b> ± 102</td> <td><b>4231</b> ± 106</td> </tr> </table>

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

ARMv7, Cortex A9, 2 hardware threads; метрика: миллионы операций в секунду, чем больше, тем лучше: <table> <tr> <td></td> <td colspan=3>1 thread</td> <td colspan=3>2 threads</td> </tr> <tr> <td></td><td>Unsafe</td><td>Safe</td><td>Tricky</td><td>Unsafe</td><td>Safe</td><td>Tricky</td> </tr> <tr> <td>Synchronized</td> <td><b>7.1</b> ± 0.1</td> <td><b>7.1</b> ± 0.1</td> <td><b>7.1</b> ± 0.1</td> <td><b>1.9</b> ± 0.1</td> <td><b>1.9</b> ± 0.1</td> <td><b>1.9</b> ± 0.1</td> </tr>

<tr> <td>NonVolatileDCL</td> <td><b>23.6</b> ± 0.1</td> <td><b>23.6</b> ± 0.1</td> <td><b>23.6</b> ± 0.1</td> <td><b>45.5</b> ± 1.8</td> <td><b>47.0</b> ± 0.1</td> <td><b>47.0</b> ± 0.1</td> </tr>

<tr> <td>VolatileDCL</td> <td><b>13.4</b> ± 0.1</td> <td><b>13.4</b> ± 0.1</td> <td><b>13.4</b> ± 0.1</td> <td><b>26.6</b> ± 0.1</td> <td><b>26.6</b> ± 0.1</td> <td><b>26.6</b> ± 0.1</td> </tr>

<tr> <td>VolatileCachedDCL</td> <td><b>17.4</b> ± 0.1</td> <td><b>17.4</b> ± 0.1</td> <td><b>17.4</b> ± 0.1</td> <td><b>34.6</b> ± 0.1</td> <td><b>34.6</b> ± 0.1</td> <td><b>34.6</b> ± 0.1</td> </tr>

<tr> <td>Holder</td> <td><b>24.2</b> ± 0.1</td> <td><b>24.2</b> ± 0.1</td> <td><b>24.2</b> ± 0.1</td> <td><b>47.8</b> ± 0.8</td> <td><b>47.9</b> ± 0.8</td> <td><b>48.0</b> ± 0.1</td> </tr>

<tr> <td>FinalWrapperDCL</td> <td><b>24.2</b> ± 0.1</td> <td><b>24.2</b> ± 0.1</td> <td><b>24.2</b> ± 0.1</td> <td><b>48.1</b> ± 0.1</td> <td><b>46.8</b> ± 2.2</td> <td><b>46.8</b> ± 2.3</td> </tr> </table>

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

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

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

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