Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Effective Java Programming Language Guide - Bloch J..pdf
Скачиваний:
41
Добавлен:
24.05.2014
Размер:
2.93 Mб
Скачать

Effective Java: Programming Language Guide

expensive to initialize and may not be needed, but will be used intensively if it is needed. This idiom is shown below:

// The initialize-on-demand holder class idiom private static class FooHolder {

static final Foo foo = new Foo();

}

public static Foo getFoo() { return FooHolder.foo; }

The idiom takes advantage of the guarantee that a class will not be initialized until it is used [JLS, 12.4.1]. When the getFoo method is invoked for the first time, it reads the field FooHolder.foo, causing the FooHolder class to get initialized. The beauty of this idiom is that the getFoo method is not synchronized and performs only a field access, so lazy initialization adds practically nothing to the cost of access. The only shortcoming of the idiom is that it does not work for instance fields, only for static fields.

In summary, whenever multiple threads share mutable data, each thread that reads or writes the data must obtain a lock. Do not let the guarantee of atomic reads and writes deter you from performing proper synchronization. Without synchronization, there is no guarantee as to which, if any, of a thread's changes will be observed by another thread. Liveness and safety failures can result from unsynchronized data access. Such failures will be extremely difficult to reproduce. They may be timing dependent and will be highly dependent on the details of the JVM implementation and the hardware on which it is running.

The use of the volatile modifier constitutes a viable alternative to ordinary synchronization under certain circumstances, but this is an advanced technique. Furthermore, the extent of its applicability will not be known until the ongoing work on the memory model is complete.

Item 49: Avoid excessive synchronization

Item 48 warns of the dangers of insufficient synchronization. This item concerns the opposite problem. Depending on the situation, excessive synchronization can cause reduced performance, deadlock, or even nondeterministic behavior.

To avoid the risk of deadlock, never cede control to the client within a synchronized method or block. In other words, inside a synchronized region, do not invoke a public or protected method that is designed to be overridden. (Such methods are typically abstract, but occasionally they have a concrete default implementation.) From the perspective of the class containing the synchronized region, such a method is alien. The class has no knowledge of what the method does and no control over it. A client could provide an implementation of an alien method that creates another thread that calls back into the class. The newly created thread might then try to acquire the same lock held by the original thread, which would cause the newly created thread to block. If the method that created the thread waits for the thread to finish, deadlock results.

To make this concrete, consider the following class, which implements a work queue. This class allows clients to enqueue work items for asynchronous processing. The enqueue method may be invoked as often as necessary. The constructor starts a background thread that removes items from the queue in the order they were enqueued and processes them by

145

Effective Java: Programming Language Guide

invoking the processItem method. When the work queue is no longer needed, the client invokes the stop method to ask the thread to terminate gracefully after completing any work item in progress.

public abstract class WorkQueue {

private final List queue = new LinkedList(); private boolean stopped = false;

protected WorkQueue() { new WorkerThread().start(); }

public final void enqueue(Object workItem) { synchronized (queue) {

queue.add(workItem);

queue.notify();

}

}

public final void stop() { synchronized (queue) {

stopped = true; queue.notify();

}

}

protected abstract void processItem(Object workItem) throws InterruptedException;

// Broken - invokes alien method from synchronized block! private class WorkerThread extends Thread {

public void run() {

while (true) { // Main loop synchronized (queue) {

try {

while (queue.isEmpty() && !stopped) queue.wait();

} catch (InterruptedException e) { return;

}

if (stopped) return;

Object workItem = queue.remove(0); try {

processItem(workItem); // Lock held!

} catch (InterruptedException e) { return;

}

}

}

}

}

}

To use this class, you must subclass it to provide an implementation of the abstract processItem method. For example, the following subclass prints out each work item, printing no more than one item per second, no matter how frequently items are enqueued:

146

Effective Java: Programming Language Guide

class DisplayQueue extends WorkQueue { protected void processItem(Object workItem)

throws InterruptedException { System.out.println(workItem); Thread.sleep(1000);

}

}

Because the WorkQueue class invokes the abstract processItem method from within a synchronized block, it is subject to deadlock. The following subclass will cause it to deadlock by the means described above:

class DeadlockQueue extends WorkQueue {

protected void processItem(final Object workItem) throws InterruptedException {

// Create a new thread that returns workItem to queue Thread child = new Thread() {

public void run() { enqueue(workItem); }

};

child.start(); child.join(); // Deadlock!

}

}

This example is contrived because there is no reason for the processItem method to create a background thread, but the problem is real. Invoking externally provided methods from within synchronized blocks has caused many deadlocks in real systems such as GUI toolkits. Luckily it is easy to fix the problem. Simply move the method invocation outside of the synchronized block, as shown:

// Alien method outside synchronized block - "Open call" private class WorkerThread extends Thread {

public void run() {

while (true) { // Main loop Object workItem = null; synchronized (queue) {

try {

while (queue.isEmpty() && !stopped) queue.wait();

} catch (InterruptedException e) { return;

}

if (stopped) return;

workItem = queue.remove(0);

}

try {

processItem(workItem); // No lock held

} catch (InterruptedException e) { return;

}

}

}

}

147

Effective Java: Programming Language Guide

An alien method invoked outside of a synchronized region is known as an open call [Lea00, 2.4.1.3]. Besides preventing deadlocks, open calls can greatly increase concurrency. An alien method might run for an arbitrarily long period, during which time other threads would unnecessarily be denied access to the shared object if the alien method were invoked inside the synchronized region.

As a rule, you should do as little work as possible inside synchronized regions. Obtain the lock, examine the shared data, transform the data as necessary, and drop the lock. If you must perform some time-consuming activity, find a way to move the activity out of the synchronized region.

Invoking an alien method from within a synchronized region can cause failures more severe than deadlocks if the alien method is invoked while the invariants protected by the synchronized region are temporarily invalid. (This cannot happen in the broken work queue example because the queue is in a consistent state when processItem is invoked.) Such failures do not involve the creation of a new thread from within the alien method; they occur when the alien method itself calls back in to the faulty class. Because locks in the Java programming language are recursive, such calls won't deadlock as they would if they were made by another thread. The calling thread already holds the lock, so the thread will succeed when it tries to acquire the lock a second time, even though there is another conceptually unrelated operation in progress on the data protected by the lock. The consequences of such a failure can be catastrophic; in essence, the lock has failed to do its job. Recursive locks simplify the construction of multithreaded object-oriented programs, but they can turn liveness failures into safety failures.

The first part of this item was about concurrency problems. Now we turn our attention to performance. While the cost of synchronization has plummeted since the early days of the Java platform, it will never vanish entirely. If a frequently used operation is synchronized unnecessarily, it can have significant impact on performance. For example, consider the classes StringBuffer and BufferedInputStream. These classes are thread-safe (Item 52) but are almost always used by a single thread, so the locking they do is usually unnecessary. They support fine-grained methods, operating at the individual character or byte level, so not only do these classes tend to do unnecessary locking, but they tend to do a lot of it. This can result in significant performance loss. One paper reported a loss close to 20 percent in a realworld application [Heydon99]. You are unlikely to see performance losses this dramatic caused by unnecessary synchronization, but 5 to 10 percent is within the realm of possibility.

Arguably this belongs to the “small efficiencies” that Knuth says we should forget about (Item 37). If, however, you are writing a low-level abstraction that will generally be used by a single thread or as a component in a larger synchronized object, you should consider refraining from synchronizing the class internally. Whether or not you decide to synchronize a class, it is critical that you document its thread-safety properties (Item 52).

It is not always clear whether a given class should perform internal synchronization. In the nomenclature of Item 52, it is not always clear whether a class should be made thread-safe or thread-compatible. Here are a few guidelines to help you make this choice.

If you're writing a class that will be used heavily in circumstances requiring synchronization and also in circumstances where synchronization is not required, a reasonable approach is to provide both synchronized (thread-safe) and unsynchronized (thread-compatible) variants.

148