Pitfalls and false optimizations

As in previous chapters, we will finish up with a discussion of obvious caveats. This section discusses things to be aware of when working with threads and synchronization in Java.

Thread.stop, Thread.resume and Thread.suspend

The single most dangerous part of the Java thread API, are the methods in the java.lang.Thread class called stop, resume, and suspend. They were included in Java 1.0, but immediately found unsafe and deprecated. This however, was a bit too late, and, even today, they are widely used both in legacy code and new applications, despite the deprecation warnings. We are sad to report that we've come across them in commercial code that was developed as late as 2008.

The stop method (meant to halt the execution of a thread) is unsafe. This is because stopping the execution of a thread that is modifying global data would possibly leave the global data in an inconsistent, broken state. A thread that receives a stop signal will unlock all of the locks that it was holding, thus making the data under modification by these locks briefly visible to the rest of the world, which violates the Java sandbox model.

Stopping threads should instead be handled by wait / notify or (volatile) variables, properly synchronized when this needs to be the case.

What about suspension? Suspending a thread is inherently deadlock prone. That is, if a thread is holding a lock and then is suspended, no other thread can access the resource protected by the lock until the suspended thread is resumed. If the thread responsible for the resume call, waking up the suspended thread, tries to acquire that lock, a deadlock will occur. Thus, Thread.resume and Thread.suspend are deemed too dangerous to leave to the user and were deprecated as well.

Consequently, never use Thread.stop, Thread.resume or Thread.suspend in any program and be aware of their issues when working with legacy code.

Double checked locking

Lack of understanding of the underlying memory model and CPU architecture, can cause trouble in the highest levels of platform-independent Java as well. Consider the following thread safe code that returns a singleton object, instantiated once only upon demand:

public class GadgetHolder {
private Gadget theGadget;
public synchronized Gadget getGadget() {
if (this.theGadget == null) {
this.theGadget = new Gadget();
}
return this.theGadget;
}
}

The previous example works fine for multiple threads, as the method is synchronized, using the GadgetHolder instance itself as the monitor. However, when the Gadget constructor has run once, further synchronization might seem unnecessary and expensive. Therefore, one might be tempted to optimize the method as:

public Gadget getGadget() {
if (this.theGadget == null) {
synchronized(this) {
if (this.theGadget == null) {
this.theGadget = new Gadget();
}
}
}
return this.theGadget;
}

The previous optimization might seem like a clever trick. If the object exists, which will be the usual case, we can return it immediately without synchronization. The singleton instantiation is still synchronized, including the original null check, retaining thread safety.

The problem here is that we have created a common anti-pattern known as double checked locking. Now, one thread can start initializing the Gadget field upon completing the inner null check in the synchronization. This thread might start allocating and writing the object to the Gadget field, which may well be a non-atomic process, containing several writes to memory without guaranteed ordering. If this happens in the middle of a context switch, another thread can come in, see the partially written object in the field and thus fail the first null check. Then the partially allocated object may be returned. The same thing can happen not just with objects, but with other field types as well. For example, longs on a 32-bit platform often need to be initialized by two 32-bit writes to memory. A 32-bit int, on the other hand (just one memory write on initialization) would slip past the trap.

The problem may, though only in the new version of the Java Memory Model, be gotten around by declaring the theGadget field volatile, but this incurs overhead anyway. Possibly less overhead than the original synchronization, but still overhead. For clarity, and because underlying memory model implementations may not be correct, double checked locking should be avoided! There are several good web pages explaining why double checked locking should be considered an anti-pattern in several languages, not just Java.

Note

The danger with problems like this is that on strong memory models they rarely break down. Intel IA-64 deployment is a typical real-world scenario where Java applications that previously have been running flawlessly start malfunctioning. Intel IA-64 has a notoriously weak memory model. It is all too easy to suspect a bug in the JVM instead of in the Java program if it runs fine on x86 but breaks on IA-64.

For static singletons, initialization can be performed with initialize on demand, providing the same semantics and avoiding double checked locking.

public class GadgetMaker {
public static Gadget theGadget = new Gadget();
}

Java guarantees that the class initialization is atomic, and as the GadgetMaker class has no other contents, theGadget will always be atomically assigned when the class is first referenced. This works both in the old and the new memory model.

In conclusion, there are plenty of caveats when programming parallel Java, but most of them can be avoided by understanding the Java Memory Model. Furthermore, even if you don't care about the underlying hardware, not understanding the Java Memory Model can still be a sure way of shooting yourself in the foot.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.65.130