4.2 Thread-Safe Classes
When designing a class that may be used for concurrent
programmingthat is, a class whose instances may be used by
more than one thread at a timeit is imperative that you make
sure the class is "thread-safe." Consider
the IntList class of Example 2-7.
This class is not thread safe. Imagine what could happen if one
thread called clear( ) while another thread was
calling add( ). If the clear( )
method sets the list size to 0 after add( ) has
read the list size but before it has stored the incremented list size
back into the size field of the
IntList, it may appear as if the call to
clear( ) never happened! In general, a thread-safe
class ensures that no thread can ever observe its instances in an
inconsistent state.There are several approaches to thread safety. A particularly simple
one is to design
immutable classes: if the state of an
object can never change, then no thread can ever observe the object
in an inconsistent state. Some classes, such as
IntList, must be mutable, however. To make these
classes thread-safe, you must prevent concurrent access to the
internal state of an instance by more than one thread. Because Java
was designed with threads in mind, the language provides the
synchronized modifier, which does just that. When
an instance method is declared synchronized, a
thread must obtain a lock on the instance before it calls the method.
If the lock is already held by another thread, the thread blocks
until it can obtain the lock it needs. This ensures that only one
thread may call any of the synchronized methods of the instance at a
time.Example 4-2 is a simplified version of the
IntList class of Example 2-7
whose methods have been declared synchronized.
This prevents two threads from calling the add( )
method at the same time, and also prevents a thread from calling
clear( ) while another thread is calling
add( ). The synchronized
keyword can also be applied to arbitrary blocks of code within a
method, simply by specifying the object to be locked before the code
is executed. The ThreadSafeIntList(
) copy constructor uses this
technique to synchronize access to the internal state of the object
it is copying.Note that it is not good design to declare every method of every
class synchronized. Calling a synchronized method
is substantially slower than calling a nonsynchronized one because of
the overhead of object locking. The
java.util.Vector class that shipped with the
original version of Java has synchronized methods to guarantee thread
safety. But most applications do not require thread safety, and Java
1.2 provided the more efficient unsynchronized alternative
java.util.ArrayList.
Example 4-2. ThreadSafeIntList.java
package je3.thread;
/**
* A growable array of int values, suitable for use with multiple threads.
**/
public class ThreadSafeIntList {
protected int[ ] data; // This array holds the integers
protected int size; // This is how many it current holds
// Static final values are constants. This one is private.
private static final int DEFAULT_CAPACITY = 8;
// Create a ThreadSafeIntList with a default capacity
public ThreadSafeIntList( ) {
// We don't have to set size to zero because newly created objects
// automatically have their fields set to zero, false, and null.
data = new int[DEFAULT_CAPACITY]; // Allocate the array
}
// This constructor returns a copy of an existing ThreadSafeIntList.
// Note that it synchronizes its access to the original list.
public ThreadSafeIntList(ThreadSafeIntList original) {
synchronized(original) {
this.data = (int[ ]) original.data.clone( );
this.size = original.size;
}
}
// Return the number of ints stored in the list
public synchronized int size( ) { return size; }
// Return the int stored at the specified index
public synchronized int get(int index) {
if (index < 0 || index >= size) // Check that argument is legitimate
throw new IndexOutOfBoundsException(String.valueOf(index));
return data[index];
}
// Append a new value to the list, reallocating if necessary
public synchronized void add(int value) {
if (size == data.length) setCapacity(size*2); // realloc if necessary
data[size++] = value; // add value to list
}
// Remove all elements from the list
public synchronized void clear( ) { size = 0; }
// Copy the contents of the list into a new array and return that array
public synchronized int[ ] toArray( ) {
int[ ] copy = new int[size];
System.arraycopy(data, 0, copy, 0, size);
return copy;
}
// Reallocate the data array to enlarge or shrink it.
// Not synchronized, because it is always called from synchronized methods.
protected void setCapacity(int n) {
if (n == data.length) return; // Check size
int[ ] newdata = new int[n]; // Allocate the new array
System.arraycopy(data, 0, newdata, 0, size); // Copy data into it
data = newdata; // Replace old array
}
}