Saturday, March 27, 2010

On why double checked locking is an anti-pattern

If you are a Java programmer by now you are probably aware of the double checked locking concurrency anti-pattern.
public final class ResourceAccessor {
private static Resource r;

public static Resource getResource(){
if(r == null) {
synchronized(ResourceAccessor.class){
if(r == null) {
r = new Resource();
}
}
}
return r;
}
}

There are several variation of double check locking code style, but the important lines of code are 5 and 7. The check for resource before obtaining a lock on line 5 and a second check on line 7.

This fancy trick employed specifically in early version of Java to optimize code to avoid (then) expensive lock operations has been found to be wrong. Often the best explanations will tell you that "a second thread may see a partially constructed resource object".

You might also find the following alternative of a correct implementation
public final class ResourceAccessor {
private static volatile Resource r;

public static Resource getResource(){
if(r == null) {
synchronized(ResourceAccessor.class){
if(r == null) {
r = new Resource();
}
}
}
return r;
}
}
The reason for both the correct and incorrect behavior are rooted in the Java Memory Model, which can be found at Chapter 17 of the Java Language Specification.

Unfortunately most explanation stop there, along with, on occasion a reference to the "happens before" ordering referenced by the Java Memory Model.

We will dig a little deeper to try to better understand the reason for why double checked locking is incorrect and also look at why using a volatile reference to the variable fixes it.

Abstractions while necessary to hide complexity in large systems also prevent us from gaining a better understanding the behavior of the systems. We will use java byte code in an attempt to help us understand the reasons, it serves our purpose in this instance, however be aware that byte code is only an abstraction.

Java does not enforce a full execution order on code, it provides only a partial ordering defined in Chapter 17 of the language specification as happens-before. Quite simply it mean the results of a write by one thread are guaranteed to be visible to a read by another thread only if the write operation happens-before the read operation. Intuitively this is correct, however how does a programmer enforce a happens-before ordering in code.

Happens-before ordering can be enforced using one of the synchronization mechanism provided by Java
  • Locks, either intrinsic locking using synchronized or using locks from java.util.concurrent which have the same semantics as intrinsic locking
  • volatile, reads and write to volatile variables are atomic.
  • Thread.start() and Thread.join() method invocation
A more refined version of happen-before ordering is defined in the package javadoc for java.util.concurrent as follows
  • Each action in a thread happens-before every action in that thread that comes later in the program's order.
  • An unlock (synchronized block or method exit) of a monitor happens-before every subsequent lock (synchronized block or method entry) of that same monitor. And because the happens-before relation is transitive, all actions of a thread prior to unlocking happen-before all actions subsequent to any thread locking that monitor.
  • A write to a volatile field happens-before every subsequent read of that same field. Writes and reads of volatile fields have similar memory consistency effects as entering and exiting monitors, but do not entail mutual exclusion locking.
  • A call to start on a thread happens-before any action in the started thread.
  • All actions in a thread happen-before any other thread successfully returns from a join on that thread.

Taking a closer look at the original double checked locking code (reproduced here)
public final class ResourceAccessor {
private static Resource r;

public static Resource getResource(){
if(r == null) {
synchronized(ResourceAccessor.class){
if(r == null) {
r = new Resource();
}
}
}
return r;
}
}
It would seems that the rule
  • Each action in a thread happens-before every action in that thread that comes later in the program's order.
would require that we see only references to resource r that are fully constructed. Since the program order requires that a new Resource() be created before being assigned to variable r.

As mentioned previously the Java language is an abstraction over the byte code specification, and what seems like 2 statements from the Java language perspective is anything but

The single statement translates to the following byte code

18: new #4; //class ResourceAccessor$Resource
21: dup
22: aconst_null
23: invokespecial #5; //Method ResourceAccessor$Resource."

":(LResourceAccessor$1;)V
26: putstatic #2; //Field r:LResourceAccessor$Resource;

Line 18 allocates memory
Line 23 invokes the constructor
Line 26 sets the reference to field r

It is possible that the reference to field r be set prior to execution of the constructor (change execution order of lines 23 and 26) all along staying within the constraints enforced by happens-before. In fact there is at least one example of such an occurrence.

The reason that the following code
public final class ResourceAccessor {
private static volatile Resource r;

public static Resource getResource(){
if(r == null) {
synchronized(ResourceAccessor.class){
if(r == null) {
r = new Resource();
}
}
}
return r;
}
}
executes correctly is because of the rule
  • A write to a volatile field happens-before every subsequent read of that same field. Writes and reads of volatile fields have similar memory consistency effects as entering and exiting monitors, but do not entail mutual exclusion locking.
which requires that the thread attempting to read variable r block until such time as resource referenced by r is not fully constructed.


No comments:

Post a Comment