Хабр Курсы для всех
РЕКЛАМА
Практикум, Хекслет, SkyPro, авторские курсы — собрали всех и попросили скидки. Осталось выбрать!
// Работает с новой семантикой volatile
// Не работает в Java 1.4 и более ранних версиях из-за семантики volatile
class Foo {
private volatile Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null)
helper = new Helper();
}
}
return helper;
}
// и остальные члены класса…
}
«Вот есть реализация DCL, реализована правильно, но не нужна, так как сам метод ничего не возвращает, к тому же использование вот тут… неправильное, так как вот тут… возникнет проблема такая-то.»
… а не приводить в примеры код и отправлять на ссылку к исходникам
правда метод должен возвращать хоть что-то, иначе смысл его использования в примере теряется
(TZDBTimeZoneNames.class) {… use TZDB_NAMES_TRIE here ...}
Ну так ткните меня носом в место в статье, где DCL в первом примере реализован неправильно, пожалуйста.
По факту публикация объекта TZDB_NAMES_TRIE состоялась в момент volatile-записи: после этого вызовы prepareFind в других потоках будут сразу выходить без синхронизации. При этом после публикации производится множество дополнительных шагов по инициализации.
Исправляются такие вещи очень легко: надо сохранить объект в локальную переменную, с её помощью вызвать все необходимые методы для инициализации, а уже потом записать volatile-поле.Я это уже встречал при разборе получающегося байт-кода и кода, сгенерированного JIT-компилятором: сначала волатильному полю присваивается указатель на выделенный кусок памяти, а следующим шагом вызывается конструктор. Так что, если делать без посредства локальной переменной, существует такой промежуток времени, в течение которого значение волатильного поля уже заполнено, но объект ещё не инициализирован, что и приводит к некорректной работе.
C tc = // initialize object + metadata
tc.f = 42; // initialize final field
[LoadStore|StoreStore]
c = tc; // publish
Особенность DCL-паттерна в том, что момент публикации объекта — это операция volatile-записи, а не выход из секции синхронизации. Поэтому именно volatile-запись должна производиться после полной инициализации объекта.
Кроме того — завершение секции synchronize приведёт к записи переменной, т.к. является барьером синхронизации в Java, разве нет?Вы меня этим озадачили. Так-то оно так, звучит разумно. Я поисследую вопрос.
volatile int dclFlag; // int в Java на всех существующих платформах атомарный, а boolean - нет.
MTObject mtObject; // Объект, с которым идёт работа в нескольких потоках.
MTObject2 mtObject2; // Ещё объекты.
void doMultithreaded() {
if (dclFlag==0) { // Один флаг на несколько объектов.
synchronized (syncObj) {
if (dclFlag==0) {
// Инициализация.
mtObject = new MTOject();
mtObject2 = new MTOject2();
dclFlag = 1; // На большинстве платформ есть атомарная запись единицы. Например ~0 далеко не все платформы умеют атомарно одной командой писать
};
};
// Код работы с переменными.
for (int i=0; i!=1000; i++)
mtObject.doSomething(); // В случае если mtObject volatile, то это превращается в ад.
};
volatile String o1="123";
int counter=0;
void test() {
for( int i=0; i!=1000; i++ ) {
if( o2.length()==3 ) // Опаньки! Кроме того, что на каждой итерации будет проверка счётчика ссылок, так volatile чтение ещё и барьер оптимизации!
counter++; // Вот тут тоже будет неэффективный и неоптимизируемый код.
};
}
Object o1;
volatile Object o2;
private void m1() {
o1 = ...;
o2 = ...; // volatile write, happens before reading o2,
// so writting o1 happens before reading o2 (transitive)
}
private void m2() {
// m1() in other thread
o1.toString(); // unsafe, no happens before relation to o1 write
}
private void m3() {
// m1() in other thread
o1.toString(); // can be either visible or not
o2.toString();
}
private void m4() {
// m1() in other thread
o2.toString(); // volatile read, o1 initialization is visible on next string
o1.toString();
}
Подумайте сами: для какой-либо синхронизации, вы в любом случае должны сделать хоть одно volatile чтение чего угодно.Строго говоря не чего угодно, а той же переменной. Если смотреть в новый jls8, 17.4.4-5, то там сказано
A write to a volatile variable v (§8.3.1.4) synchronizes-with all subsequent reads of v by any thread (where «subsequent» is defined according to the synchronization order).
Приведите пример реально полезного DCL?В Scala с помощью DCL сделаны lazy-значения ) Т.е., насколько я понимаю, компилятор разворачивает
lazy val abc = ... в DCL с битовым флагом.MTObject mtObject;
MTObject2 mtObject2;
void doMultithreaded() {
synchronized (syncObj) {
if (mtObject==null) {
// Инициализация.
mtObject = new MTOject();
mtObject2 = new MTOject2();
}
}
// Код работы с переменными.
for (int i=0; i!=1000; i++)
m1.doSomething();
};
// В случае если mtObject volatile, то это превращается в ад.
// это глобально видимая ссылка на объект
volatile MTObject mtObject;
...
MTObject threadMTObject = mtObject;
...
for (int i=0; i!=1000; i++)
threadMTObject.doSomething();
Кроме того — завершение секции synchronize приведёт к записи переменной, т.к. является барьером синхронизации в Java, разве нет?Я поизучал и поспрашивал на SO. Кратко:
volatile тут таки необходим.class Foo {
private volatile Bar _barInstance;
public Bar getBar() {
if (_barInstance == null) {
synchronized(this) { // or synchronized(someLock)
if (_barInstance == null) {
Bar newInstance = new Bar();
// possible additional initialization
_barInstance = newInstance;
}
}
}
return _barInstance;
}
}
_barInstance может быть присвоена раньше, чем выполнятся инструкции конструктора. Другой поток может увидеть, что _barInstance и, соответственно, не брать lock. А значит happens-before на этом блоке синхронизации не случится и появляется риск использования недоинициализированного объекта.DCLVarType dclVar;
DCLVarType getDCLVar() {
if(dclVAR!=null)
return dclVar;
synchronized(syncVar) {
if(dclVAR!=null)
return dclVar;
dclVar = new DCLVarType(); // Тут можно долго и сложно конструировать через промежуточную переменную.
return dclVar;
};
}
Ещё раз (надеюсь, последний) про double-checked locking