Juan-Carlos Gandhi (
juan_gandhi) wrote2007-04-11 02:48 pm
![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
java synchronization gone wild and back
Hey, so, you, like everyone else, decided to make your static initialization thread-safe. You have a singleton that is hard to instantiate, and You have something like this:
and it does not work, because, say, a dozen of servlet threads in parallel are trying to reinitialize your singleton like crazy; a minute that it takes to instantiate one, turns into 12 minutes, since all 12 are trying to do the same.
Okay, you synchronize it:
In this case you lock execution on the whole class; your class may have a bunch of other singletons, and they will all stand in line waiting for this specific singleton to be retrieved - looks stupid, right? so you introduce a gentle check:
See, before instantiating, you check whether it is instantiated already.
The trouble is, your twelve threads will get through this check and wait on the same barrier before synchronized block, and then will instantiate the singleton, one after another, taking 12 minutes in total. Okay, you get angry, you open a book and steal a solution, the famous double-check trick:
Now you feel good, the threads would not reinitialize the same singleton, because when the second threads penetrates the synchronized block, worker is already not null. You feel good... but then you open Josh and Neal's Java Puzzlers, and see that oops, this won't work on a multiprocessor machine. Why? See, by the time the first thread leaves the syncrhonized block, the value of worker may not reach the memory shared by the threads.
So, the solution suggested is this:
See, the trick is that the singleton is provided by a static class, which is loaded strictly once, which is guaranteed by the class loader.
Now enters "new Java 5", not sure which one, 1.5.0.6 or 1.5.0.7. Bug fixed. All you have to do is declare My worker volatile:
So, now we are back to circle 3: after a bugfix, double checks are legal again.
static My worker;
static My worker() {
if (worker == null) {
worker = new MyImplementation();
}
return worker;
}
and it does not work, because, say, a dozen of servlet threads in parallel are trying to reinitialize your singleton like crazy; a minute that it takes to instantiate one, turns into 12 minutes, since all 12 are trying to do the same.
Okay, you synchronize it:
static My worker;
static synchronized worker() {
if (worker == null) {
worker = new MyImplementation();
}
return worker;
}
In this case you lock execution on the whole class; your class may have a bunch of other singletons, and they will all stand in line waiting for this specific singleton to be retrieved - looks stupid, right? so you introduce a gentle check:
static My worker;
static worker() {
if (worker == null) {
synchronized {
worker = new MyImplementation();
}
}
return worker;
}
See, before instantiating, you check whether it is instantiated already.
The trouble is, your twelve threads will get through this check and wait on the same barrier before synchronized block, and then will instantiate the singleton, one after another, taking 12 minutes in total. Okay, you get angry, you open a book and steal a solution, the famous double-check trick:
static My worker;
static worker() {
if (worker == null) {
synchronized {
if (worker == null) {
worker = new MyImplementation();
}
}
}
return worker;
}
Now you feel good, the threads would not reinitialize the same singleton, because when the second threads penetrates the synchronized block, worker is already not null. You feel good... but then you open Josh and Neal's Java Puzzlers, and see that oops, this won't work on a multiprocessor machine. Why? See, by the time the first thread leaves the syncrhonized block, the value of worker may not reach the memory shared by the threads.
So, the solution suggested is this:
static My worker;
private interface HR {
My getWorker();
}
private static HR catbert = new HR() {
My worker = new MyImplementation();
My getWorker() {
return worker;
}
}
static worker() {
return catbert.getWorker();
}
See, the trick is that the singleton is provided by a static class, which is loaded strictly once, which is guaranteed by the class loader.
Now enters "new Java 5", not sure which one, 1.5.0.6 or 1.5.0.7. Bug fixed. All you have to do is declare My worker volatile:
volatile static My worker;
static worker() {
if (worker == null) {
synchronized {
if (worker == null) {
worker = new MyImplementation();
}
}
}
return worker;
}
So, now we are back to circle 3: after a bugfix, double checks are legal again.
no subject
no subject
no subject
no subject
no subject
no subject
no subject
no subject
if (worker == null) {
synchronized {
if (worker == null) {
О, большое спасибо -- до этого места я когда-то сам придумал, но не был уверен. И что, говорите не будет работать? Только на джаве или вообще?
no subject
no subject
no subject
no subject
no subject
no subject
no subject
worker = new MyImplementation();
все остальные процессоры не стоят? Потому что если они стоят, то worker либо null, либо показывает на нормальный объект.
no subject
no subject
no subject
no subject
no subject
http://ivan-ghandhi.livejournal.com/480761.html?thread=2363385#t2363385
no subject
no subject
* Note regarding memory visibility: Pthreads has rules about memory
* visibility and mutexes. Very roughly: Memory a thread can see when
* it unlocks a mutex can be seen by another thread that locks the
* same mutex.
То есть, второй тред, который ждет на мьютексе, войдя в мьютекс, увидит уже новое значение переменной, которое обновилось предыдущем тредом. Что в этом утверждении может быть неправильно?
no subject
первый тред проверяет указатель на ноль, входит в мьютекс, инициализирует объект.
второй тред проверяет указатель на ноль.
поскольку первый тред еще не вышел из мьютекса и не выполнил команду барьера, состояние общей памяти неконсистентно, второй тред видит ненулевой указатель, указывающий на недостроенный объект.
no subject
* A memory barrier after a lock and before an unlock will provide
* this behavior.
Врет комментарий? (пардон нет пока времени разбираться в коде)
no subject
//То есть, второй тред, который ждет на мьютексе, войдя в мьютекс, увидит уже новое значение переменной, которое обновилось предыдущем тредом.//
оно правильное, и барьеры памяти гарантируют такое поведение. но утверждение говорит о треде, который вошёл в мьютекс. а у нас имеется тред, который НЕ вошёл в мьютекс. то есть абсолютно вся память, которая разделяется тредами, должна быть защищена мьютексами, иначе ничего не гарантируется. а у нас есть указатель, который читается снаружи.
no subject
Проблема заключается в том, что ссылка на непроиницилизированный объект может появиться раньше, чем объект сконструируется, а не в том, что второй тред увидит эту ссылку до момента выхода из синхронизированного блока. И существует вероятность, что он будет работать с неинициализорованным объектом. А memory barrier между инициализацией и присвоением shared variable гарантирует, что подобной гадости не произойдет. Видимо, аналогичным образом работает и присвоение volatile переменной, которая выставляет барьер перед присваиванием.
no subject
Можно и так сказать.
А memory barrier между инициализацией и присвоением shared variable гарантирует, что подобной гадости не произойдет.
Да, если мы умеем вставлять memory barrier в произвольном месте кода, это решает проблему. К сожалению, никакой стандарт не предусматривает такой операции.
no subject
no subject
Интересно, а какова физика этого явления? Дело в том, что если в ячейку X ровно один поток имеет возможность записывать значение, а остальные потоки имеют права прочесть это значение, но строго после того, как поток значение записал, то никакой неодназначности быть не может. Единственное, что мне приходит в голову: процесс заглядывает вперед, заполняет конвеер и "подсасывает" значения переменных в процессорный кеш. И читает, соответственно, тоже из кеша.
Я прямо скажем абсолютно никакой специалист по архитектуре ЭВМ и не понимаю, а может ли такое случиться. Интересно понять, а что же происходит на самом деле.
no subject
no subject
безопасно.
(http://cvsweb.netbsd.org/bsdweb.cgi/src/lib/libpthread/pthread_mutex.c?rev=1.27&content-type=text/x-cvsweb-markup)
/*
* Note regarding memory visibility: Pthreads has rules about memory
* visibility and mutexes. Very roughly: Memory a thread can see when
* it unlocks a mutex can be seen by another thread that locks the
* same mutex.
*
* A memory barrier after a lock and before an unlock will provide
* this behavior. This code relies on pthread__simple_lock_try() to issue
* a barrier after obtaining a lock, and on pthread__simple_unlock() to
* issue a barrier before releasing a lock.
*/
Неужели synchronized в Java работает по-другому? Вот, например, в статье в Java World за 2001ый год Алан Голуб пишет буквально следующее:
"Memory barriers surround synchronization To summarize, synchronization implies a memory barrier. In that case, two exist: one barrier associated with entering a synchronized block or method and another associated with leaving it"
Неужели "гонит"?
no subject
no subject
no subject
Volatile в java, насколько я понимаю, делает этот самый memory barrier на кажду операцию доступа к volatile переменной. Таким образом, проверка
if (worker == NULL)
вне synchronized перестает, наверное, "облегченной". Вот тут, например, (http://blogs.msdn.com/brada/archive/2004/05/12/130935.aspx) говорят, что лучше не использовать volatile, а явным образом использовать memory barrier. В частности, это всегда корректно даже при отложенной инициализации.
no subject
no subject
no subject
no subject
no subject
1. статические констракторы что, святым духом обеспечивают единственность инициализации? тем же самым lock(), так что performance inefficiency решения №2 никуда не девается.
2. singleton pattern в его классическом понимании is dead. во всех случаях, когда действительно нужен единственный инстанс класса, его можно просто заменить на static class.
3. если не синглтон, а просто статический член с долгой инициализацией, то что делать?
4. ну и наконец, заявление "it's broken in .NET too" безо всяких пояснений никуда не годится. как именно broken? почему broken?
no subject
Производительность всегда можно померять.
2. В .NET инстанс статического класса создастся даже тогда, когда есть вызовы его статических членов. Если у нас есть статические члены не связанные по функциональности с нашим "синглтоном", то хотелось бы развязать вызовы этих членов и создании инстанса класса. С другой стороны понятно, что это кривизна дизайна.
3. Так этот вопрос совершенно не касается singleton. Возможно в этом случае lock является лучшим вариантом.
4. Согласен.
no subject
2. В .NET инстанс статического класса создастся даже тогда, когда есть вызовы его статических членов.
мнэ... huh?
0. на самом деле я, конечно, погорячился. просто обидно стало, что годами проверенный код вдруг так походя записали в bad. действительно интересно, чем обеспечивается единственность вызова статического констрактора - но даже если тем же локом или unmanaged аналогом, то реализация со вложенным классом всё равно мне больше нравится, несмотря даже на "лишний" элемент в скопе. а в джаве так и этого минуса нет, там вложенный класс виден внешнему, даже если он private.
no subject
no subject
Что там народ из CLR team говорит по этому поводу?
no subject
no subject
no subject
я правильно угадал концовку?
no subject
no subject
(до чего же все-таки джава ебанутая)
no subject
no subject
Now you feel good, the threads would not reinitialize the same singleton, because when the second threads penetrates the synchronized block, worker is already not null. You feel good... but then you open Josh and Neal's Java Puzzlers, and see that oops, this won't work on a multiprocessor machine. Why? See, by the time the first thread leaves the syncrhonized block, the value of worker may not reach the memory shared by the threads.
>>
Nope. The only real reason could be that the Java compiler remembers the value of "worker" in the virtual machine registers/stack before it gets the mutex, and then when the mutex synchronizes the memory, the old value from the registers continues to be used throughout the constructor. That's why "volatile" fixes it: it requires that the value gets pulled from the memory afresh every time. I would expect it from C++, but then Java has synchronization as a language statement, so it should treat these statements as barriers for storing temporary values in registers.
BTW, probably a simpler initialization is something like this (my Java nay contain syntax errors):
Overall the Java synchronized stuff is way too uglier than it seems at first :-( And singletons are Evil.
no subject
yes, but that's exactly why the solution with "volatile" is not the best: you incur the performance hit every time you access the member.
no subject
no subject
no subject
no subject
if what you are saying about cpu registers is true, no multithreaded code would ever work without "volatile" keyword, which is quite hard to believe.
no subject
no subject
no subject
no subject
no subject
no subject
no subject
no subject