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.