Thread-Safe Classes

When designing a class that may be used for concurrent programming—that is, a class whose instances may be used by more than one thread at a time—it 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
    }
}

Get Java Examples in a Nutshell, 3rd Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.