juan_gandhi: (Default)
Juan-Carlos Gandhi ([personal profile] juan_gandhi) wrote2007-04-11 02:48 pm

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:


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.

[identity profile] ex-ex-zhuzh.livejournal.com 2007-04-11 10:14 pm (UTC)(link)
mmm... http://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom ???

[identity profile] upstartn.livejournal.com 2007-04-11 10:31 pm (UTC)(link)

[identity profile] ivan-gandhi.livejournal.com 2007-04-11 10:44 pm (UTC)(link)
The idiom, however nice, was just a patch for a bug. :) You may call it an idiom, a hack, depending on the attitude. The point is, it would not exist if jvm worked properly with volatiles in previous versions.

[identity profile] ivan-gandhi.livejournal.com 2007-04-11 10:46 pm (UTC)(link)
Oh shit... you wrote it 8 months ago! Pity I was not subscribed to your blog back then.

[identity profile] birdwatcher.livejournal.com 2007-04-11 11:07 pm (UTC)(link)
if (worker == null) {
    synchronized {
        if (worker == null) {


О, большое спасибо -- до этого места я когда-то сам придумал, но не был уверен. И что, говорите не будет работать? Только на джаве или вообще?

[identity profile] ivan-gandhi.livejournal.com 2007-04-11 11:14 pm (UTC)(link)
Теперь будет! :)

[identity profile] ex-ex-zhuzh.livejournal.com 2007-04-11 11:17 pm (UTC)(link)
Well, whatever it is, isn't it actually nicer than the double-check idiom (new and improved, now with one fewer bug)?

[identity profile] spamsink.livejournal.com 2007-04-11 11:26 pm (UTC)(link)
I'd say that the bug and the idiom are independent. Having 'synchronized' without 'volatile' obviously was an idiocy, but why should I bother writing explicit synchronization incantations and runtime hints to initialize a singleton if the runtime already provides the mechanism. IBM (linked by [livejournal.com profile] upstartn) says the same thing:


Instead of double-checked locking, use the Initialize-on-demand Holder Class idiom, which provides lazy initialization, is thread-safe, and is faster and less confusing than double-checked locking[.]

[identity profile] ex-ex-zhuzh.livejournal.com 2007-04-11 11:39 pm (UTC)(link)
«вообще» тоже не будет. скажем, вместо synchronized «вообще» бывает pthread_mutex_lock()/pthread_mutex_unlock() или эквивалент. эти операции обычно не включают memory barrier instructions. поэтому на мультипроцессорах всё поломается. нужно явно где-то добывать и вставлять эти самые memory barrier instructions. на линуксе это будет smp_mb() и семействo, на виндозе MemoryBarrier() и т.д.

[identity profile] birdwatcher.livejournal.com 2007-04-11 11:42 pm (UTC)(link)
Облом. А зачем тогда может понадобиться pthread_mutex_lock() без smp_mb() ?

[identity profile] ivan-gandhi.livejournal.com 2007-04-11 11:48 pm (UTC)(link)
In the simple and easy form above - I'd rather agree, that yes, it is cleaner. But in my real practice somehow it always looked uglier.

[identity profile] ex-ex-zhuzh.livejournal.com 2007-04-12 12:01 am (UTC)(link)
если весь доступ к переменной внутри pthread_mutex_lock(), то проблем нет. так обычно и бывает. но у нас запись внутри, а чтение-то снаружи. это не работает.

[identity profile] birdwatcher.livejournal.com 2007-04-12 12:04 am (UTC)(link)
Так для того и читаем второй раз внутри, казалось бы?

[identity profile] ex-ex-zhuzh.livejournal.com 2007-04-12 12:06 am (UTC)(link)
вот сейчас подумал и сообразил, что ерунду написал, а где именно — пока не понял.

[identity profile] ex-ex-zhuzh.livejournal.com 2007-04-12 12:12 am (UTC)(link)
когда читаем внутри, нет проблем. проблема, когда НЕ читаем, т.е. там не NULL. то есть указатель на объект уже записан в память, а состояние самого объекта ещё нет. на унипроцессоре это нормально, система гарантирует, что когда состояние понадобится, оно уже будет в памяти (система знает, когда именно оно понадобится). на мультипроцессоре это не так.

[identity profile] birdwatcher.livejournal.com 2007-04-12 12:18 am (UTC)(link)
Разве, когда выполняется эта строчка,
   worker = new MyImplementation();
все остальные процессоры не стоят? Потому что если они стоят, то worker либо null, либо показывает на нормальный объект.



[identity profile] ex-ex-zhuzh.livejournal.com 2007-04-12 12:23 am (UTC)(link)
а, понял, где ерунда: pthread_mutex_lock() как раз содержит memory barrier, но проку в этом мало — мы пытаемся читать переменную снаружи барьера!

[identity profile] ex-ex-zhuzh.livejournal.com 2007-04-12 12:24 am (UTC)(link)
не стоят, нет, с чего бы?

[identity profile] birdwatcher.livejournal.com 2007-04-12 12:44 am (UTC)(link)
Ага, увидел. Спасибо.

[identity profile] itman.livejournal.com 2007-04-12 01:42 am (UTC)(link)
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.

Интересно, а какова физика этого явления? Дело в том, что если в ячейку X ровно один поток имеет возможность записывать значение, а остальные потоки имеют права прочесть это значение, но строго после того, как поток значение записал, то никакой неодназначности быть не может. Единственное, что мне приходит в голову: процесс заглядывает вперед, заполняет конвеер и "подсасывает" значения переменных в процессорный кеш. И читает, соответственно, тоже из кеша.
Я прямо скажем абсолютно никакой специалист по архитектуре ЭВМ и не понимаю, а может ли такое случиться. Интересно понять, а что же происходит на самом деле.

[identity profile] ivan-gandhi.livejournal.com 2007-04-12 01:47 am (UTC)(link)
Да всё просто; у каждого процессора свой кеш, и всё складывать сразу в память - слишком, говорят, дорого всё всё время синхронизировать.

[identity profile] itman.livejournal.com 2007-04-12 02:41 am (UTC)(link)
Любопытно, с phtread_mutex'ами проделывать подобные операции всегда

безопасно.
(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"
Неужели "гонит"?

[identity profile] itman.livejournal.com 2007-04-12 02:43 am (UTC)(link)
Не понял, почему снаружи? Наскольку я знаю, проблема может возникнуть только в том случае, если один процесс читает "грязное" значение переменной из кеша, несмотря на то, что сам код выполняется сериализованно. А memory barriers такую ситуацию исключают.

http://ivan-ghandhi.livejournal.com/480761.html?thread=2363385#t2363385

[identity profile] itman.livejournal.com 2007-04-12 02:43 am (UTC)(link)
пардон, вот сама статья (http://www.roseindia.net/software-tutorials/detail/2715)

[identity profile] selfmade.livejournal.com 2007-04-12 03:03 am (UTC)(link)

Page 1 of 3