Этот пост сохранён в исторических целях. Его содержание и форматирование не поддерживаются ни в каком виде. Читайте апдейт: "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’е, или потенциальной стоимостью лишнего дереференса