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] birdwatcher.livejournal.com 2007-04-11 11:07 pm (UTC)(link)
if (worker == null) {
    synchronized {
        if (worker == null) {


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

[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] 109.livejournal.com 2007-04-12 06:34 am (UTC)(link)
дочитал этот крайне интересный текст до середины, дальше не хватило сил, потому что с самого начала было понятно, как писать правильно (C#):

private static object workerSyncRoot = new object();
private static My worker;
public  static My Worker
{
  get 
  {
    if (worker == null)
      lock (workerSyncRoot)
        if (worker == null)
          worker = new MyImpl();
    return worker;
  }
}


я правильно угадал концовку?

[identity profile] anspa.livejournal.com 2007-04-12 07:08 am (UTC)(link)
прямо collision detection напрашивается с рандомной задержкой от x до y миллисекунд. :)

(до чего же все-таки джава ебанутая)

[identity profile] ex-chrobin.livejournal.com 2007-04-12 08:22 am (UTC)(link)
singleton is a sick pattern

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

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):

class My;

// this class will provide synchronization for
// the initialization of class My
class MyHelper {
    static synchronized void getMy()
    {
        My::initialize();
    }
};

class My {
protected:
    friend class MyHelper;

    static My worker;

    static void initialize()
    {
        if (worker == null)
            worker = new My();
    }

public:
    static My worker() 
    {
        MyHelper::getMy();
        return worker;
    }
};


Overall the Java synchronized stuff is way too uglier than it seems at first :-( And singletons are Evil.