Tuesday, February 26, 2013

Crash Course on Multiple Threads and Java


Despite rumors to the contrary, multithreaded programming in Java is really easy -- unless you want your code to be fast and correct, of course (many other languages fare even worse, having all of the trickiness with none of the performance benefits). Having seen a lot of apps (and even some widely used web frameworks!) end up subtly wrong, horribly slow or both, I'm throwing this quick introduction together to hopefully save you the same fate.

 The Root of the Problem

Shared Memory and Race Conditions

Java threads use shared memory (whether it be RAM, disk, or network) and having one thread modify that memory while another is accessing it, whether it be through a write or a read, is where we get the race conditions that'll make you tear out your hair. This can be fairly obvious like in the case of the following counter.


public class Counter {
   private int counter;
   public void increment() { counter = counter + 1; }
}


Race conditions can also be very non-obvious and more than a little tricky to track down, like one thread adding an entry to a HashMap while another tries to retrieve an entry from a HashMap. Just to make your life a little more fun, the behavior that that exhibits is the call to get goes into an infinite loop. Oh joy.

This Whole Program's Out of Order!

The other hidden gremlin that's just waiting to throw a monkey wrench into the works is that the compiler and the CPU are perfectly free to reorder your program almost willy-nilly to get better performance (and quite a few of them absolutely will if it means that they can hide the latency of a memory access). I don't have an example handy that's not also just a pure race condition; bugs that jump out because of reordering will make your head hurt and then result in lots of wailing and gnashing of teeth. 

Stop the Madness!

Now that we know that race conditions are what makes our code wrong, how do we fix it? JSR-133 kindly gives us a beacon shining in the dark by defining Java's memory model, complete with the effects of memory synchronization points (and now you know why the keyword isn't something like critical or atomic, in case you're hanging out with people bored enough to ask!). I'll get into the JMM in a bit, but first a tip that'll make dealing with it (and getting your code correct in general).

Limit Your Scope!

The more restrictive the scope of your variables, the easier it is to reason about what modifies them and when. For example, if you don't need a variable to be at the class level (static), make it an instance variable and you've instantly reduced the ways that your variable can be messed with; if the object isn't shared, the variable gets thread-safety for free.

You can also limit your scope from a threading perspective (but not from a making the overall code easier to get right) by using ThreadLocal variables. These nifty gadgets have been around since the early days (Java 1.2), but they're not very commonly used because they're kind of weird. Looking at them, they just look like an object that holds another object, but the catch is that each thread ends up with its own values, so one thread calling set won't be visible to any other threads. The upshot to that is that if all you're modifying are thread locals, you do not need to synchronize since you're operating on memory that is not shared. 

Controlling Time

The Java Memory Model (JMM from here on out) defines its ordering in terms of happens-before and happens-after relationships, which are exactly what they sound like. If x happens before y, then if you've seen the effects of y, you'll also see the effects of x. This gives us a pretty simple way to think about our programs without having to mentally run through every possible interleaving of threads.

We get our happens-before (and happens-after) relationships using locks, synchronized blocks and volatiles (oh my!). Volatile variables are a little bit weird, so let's start simple with locking and synchronized blocks. When you acquire a lock (or enter a synchronized block), anything that happened before the lock was last released on any other thread will have happened; i.e. you've established a happens-before relationship. When you release the lock (or exit the synchronized block), then everything that you've done up to that point is done and visible to other threads.

Volatiles are kind of weird in that a read from one has the same memory effects as acquiring a lock and writing to them has the same effect as a memory release. That means you can have the memory effects of acquiring a lock without the effects of releasing one, the effects of releasing a lock without acquiring it, or even the effects of a release before an acquire (though I can't think of any sane reason why you'd want to do the last of these). 

Where volatile variables are most frequently used are as signalling variables, e.g. to tell a thread that it's time to start shutting down. As an example of using a volatile as a signalling variable, in the following code, if the variable shuttingDown is not declared volatile, then it's entirely correct for the JVM to go into an infinite loop.

while(!shuttingDown) {
   doSomeStuff();
}
In this post, we've gone over the basics of making the code correct. In the next post (which hopefully will be up in a few days), I'll go over some issues to keep in mind to actually make it fast.