Skip to main content

Answer to Weekend Multithreaded Puzzler

This is the answer to the riddle posed in my earlier puzzler posting. You should probably look at that question before looking at this answer.

I suppose I should call it something other than a puzzler, to avoid getting hit by Josh and Neal's angry team of vicious, snarling lawyers...

This program certainly looks straightforward. It just looks as if two threads are writing to two variables. In fact, you probably expected me to say something "who-cares" oriented about compiler optimizations at this point. Well, they are volatile variables, so you can worry a lot less about potential reorderings. In fact, this one has absolutely nothing to do with program transformations, and, if you ran the program on my laptop, you found that it hangs!

It turns out that this is the result of one of those vicious little static initialization quirks that provide so many hours of headaches. What happens is something very like the following. The first thread encounters A; this class has not been initialized, so that thread tries to initialize it. When you try to initialize a class, you acquire a lock on that class, so that no one else can initialize it at the same time. Are you starting to see where this could lead to problems?

At the same time as this, the second thread encounters B, and so acquires the lock on the B class. It then runs the static initializer for B, which encounters A. "Wait!" it says -- "A hasn't been initialized! Better acquire that initialization lock..." It tries to acquire the lock, but the first thread already has it, so it waits for the first thread to finish.

Meanwhile, the same process goes on in the first thread. It runs the static initializer for A, which encounters B. "Wait!" it says -- "B hasn't been initialized! Better acquire that initialization lock..." It tries to acquire the lock, but the second thread already has it, so it waits for the second thread to finish.

Result: Both threads wait forever. Deadlock!

This whole process is scheduling / hardware / OS / JVM dependent, of course. If the first thread runs to completion without letting the second thread start, then it will quite happily initialize both A and B without the other thread acquiring any locks. This will avoid deadlock nicely. This seems to happen on Linux, but not OS X.

How do you avoid this? Well, that's a little tricky. In this case, you would probably rewrite the code so that it doesn't perform the initialization in two separate threads. That's not always a general-purpose solution, though. Your best bet, in general, is to avoid having circularities like this in your static initializers. As with constructors, it is important to keep your static initializers as simple as possible.

Keeping it simple might mean not doing anything that might trigger subsequent static initialization. That's a good first option, if you can manage it, but it is not always possible.

The second option is to make sure that you have an order over your classes. For example, if you have three classes, A, B and C, you could structure them so that C can refer to A and B in its static initializer, B can only refer to A, and A can only refer to itself. This will prevent deadlocks by enforcing a strict order over when the implicit locks can be acquired.

The final option -- if you know this will be a problem -- is to make sure that the initialization can only happen in a single thread. This may mean having to force it to occur earlier, by referencing the class earlier than it would otherwise have been referenced.

I feel like there should be a moral. The moral is that static initialization has all sorts of hidden gotchas, and you should look out for it.

Comments

Bob said…
Option 4: The JVM could use a coarser-grained lock, i.e. one lock for all static initializations. Do we really gain much (if anything) from concurrent class loading?
Jeremy Manson said…
Hey Bob --

I think the most persuasive argument would be that if you had one initializer lock, you could end up with one poorly (or maliciously) written static initializer that stops every other thread from doing any class initialization, ever, without even having the cycles I have in this example!

To illustrate, imagine the same example, but replace one of the static initializers with:

static {
  while (true) {}
}

Eventually, other threads are probably going to want to do some class initialization, and so the VM might end up starving. This is probably more of a security risk than the other approach.

The one-big-lock approach would also be outside of spec (JLS, Section 12.4.2), so a random JVM couldn't do it without a fairly major JLS revision.

Ultimately, I think it is actually more intuitive to have the initializer lock on the class object instead of having one initializer lock to rule them all.
Bob said…
You could lock at the ClassLoader level and then run untrusted code in its own ClassLoader.

Also, other puzzlers already illustrate that you can lock up the VM by locking on Thread.class, etc. (I think I remember that one right).
Bob said…
And I am speculating about a change to the JLS.
Anonymous said…
If there was a per ClassLoader static initialisation lock, then I think you are more likely to end up with a potential deadlock. Static initialisation is one of those things that can happen anywhere. Including where you have a static (or static equivalent) lock held. If another static intialiser does something that requires the same lock, then you are done for.

A single static initialiser routine per module would be more like it.

==

The Thread related lock most often cited is the instance lock, which IIRC holding prevents the thread exiting and hence deadlocking in conjunction with join. (Thread exits also cause a quasi-spurious notify on the instance lock.) The lock on Thread.class itself prevents new thread being created as nextThreadID (and nextThreadNum) fail to use AtomicInteger.

Anyway, it's easy to DoS. I accidentally kept making my own X highly unresponsive on Wednesday afternoon. There's plenty of other static locks you can hold if you want a more covert DoS.
Anonymous said…
I agree with Bob!
Anonymous said…
Too easy!!
Jeremy Manson said…
(Much later)

Bob --

In the interests of not undercutting Neal and Josh's book, I will tell you that I believe that you are referring to puzzle 85, without further elaborating.

A more interesting solution: in an ideal world, you could do a short analysis of the initializer block (and transitively, of all initializer blocks that might get triggered by that block) to see what classes might be initialized. Then prevent initialization of any of those classes by any other thread while your static initializer runs. In effect, you would just be eagerly acquire all of the locks you might need.

No one else would be able to do it at the same time, of course, and if you couldn't get all of the locks, you would back off and try again.

This implementation might even be legal now, come to think of it.
Anonymous said…
I would like too take some time out Thank everyone for doing what you do and make this community great im a long time reader and first time poster so i just wanted to say thanks.

Popular posts from this blog

Double Checked Locking

I still get a lot of questions about whether double-checked locking works in Java, and I should probably post something to clear it up. And I'll plug Josh Bloch's new book, too. Double Checked Locking is this idiom: // Broken -- Do Not Use! class Foo {   private Helper helper = null;   public Helper getHelper() {     if (helper == null) {       synchronized(this) {         if (helper == null) {           helper = new Helper();         }       }     }   return helper; } The point of this code is to avoid synchronization when the object has already been constructed. This code doesn't work in Java. The basic principle is that compiler transformations (this includes the JIT, which is the optimizer that the JVM uses) can change the code around so that the code in the Helper constructor occurs after the write to the helper variable. If it does this, then after the constructing thread writes to helper, but before it actually finishes constructing the object,

What Volatile Means in Java

Today, I'm going to talk about what volatile means in Java. I've sort-of covered this in other posts, such as my posting on the ++ operator , my post on double-checked locking and the like, but I've never really addressed it directly. First, you have to understand a little something about the Java memory model. I've struggled a bit over the years to explain it briefly and well. As of today, the best way I can think of to describe it is if you imagine it this way: Each thread in Java takes place in a separate memory space (this is clearly untrue, so bear with me on this one). You need to use special mechanisms to guarantee that communication happens between these threads, as you would on a message passing system. Memory writes that happen in one thread can "leak through" and be seen by another thread, but this is by no means guaranteed. Without explicit communication, you can't guarantee which writes get seen by other threads, or even the order in whic

Date-Race-Ful Lazy Initialization for Performance

I was asked a question about benign data races in Java this week, so I thought I would take the opportunity to discuss one of the (only) approved patterns for benign races. So, at the risk of encouraging bad behavior (don't use data races in your code!), I will discuss the canonical example of "benign races for performance improvement". Also, I'll put in another plug for Josh Bloch's new revision of Effective Java (lgt amazon) , which I continue to recommend. As a reminder, basically, a data race is when you have one (or more) writes, and potentially some reads; they are all to the same memory location; they can happen at the same time; and that there is nothing in the program to prevent it. This is different from a race condition , which is when you just don't know the order in which two actions are going to occur. I've put more discussion of what a data race actually is at the bottom of this post. A lot of people think that it is okay to have a data