Java Reference
In-Depth Information
Atomic
The output errors of Fig. 23.7 can be attributed to the fact that the shared object,
Simple-
Array
, is not
thread safe
—
SimpleArray
is susceptible to errors if it's
accessed concurrently
by multiple threads
. The problem lies in method
add
, which stores the value of
writeIndex
,
places a new value in that element, then increments
writeIndex
. Such a method would
present no problem in a single-threaded program. However, if one thread obtains the value
of
writeIndex
, there's no guarantee that another thread cannot come along and increment
writeIndex
before
the first thread has had a chance to place a value in the array. If this hap-
pens, the first thread will be writing to the array based on a
stale value
of
writeIndex
—a
value that's no longer valid. Another possibility is that one thread might obtain the value
of
writeIndex
after
another thread adds an element to the array but
before
writeIndex
is
incremented. In this case, too, the first thread would write to the array based on an invalid
value for
writeIndex
.
SimpleArray
is
not thread safe because it allows any number of threads to read and modify
shared mutable data concurrently
, which can cause errors. To make
SimpleArray
thread safe,
we must ensure that no two threads can access its shared mutable data at the same time.
While one thread is in the process of storing
writeIndex
, adding a value to the array, and
incrementing
writeIndex
,
no other thread
may read or change the value of
writeIndex
or
modify the contents of the array at any point during these three operations. In other words,
we want these three operations—storing
writeIndex
, writing to the array, incrementing
writeIndex
—to be an
atomic operation
, which cannot be divided into smaller subopera-
tions. (As you'll see in later examples, read operations on shared mutable data should also be
atomic.) We can simulate atomicity by ensuring that only one thread carries out the three
operations at a time. Any other threads that need to perform the operation must
wait
until
the first thread has finished the
add
operation in its entirety.
Atomicity can be achieved using the
synchronized
keyword. By placing our three
suboperations in a
synchronized
statement or
synchronized
method, we allow only one
thread at a time to acquire the lock and perform the operations. When that thread has
completed all of the operations in the
synchronized
block and releases the lock, another
thread may acquire the lock and begin executing the operations. This ensures that a thread
executing the operations will see the actual values of the shared mutable data and that
these
values will not change unexpectedly in the middle of the operations as a result of another thread's
modifying them
.
Software Engineering Observation 23.5
Place all accesses to mutable data that may be shared by multiple threads inside
synchronized
statements or
synchronized
methods that synchronize on the same lock.
When performing multiple operations on shared mutable data, hold the lock for the
entirety of the operation to ensure that the operation is effectively atomic.
Class
SimpleArray
with Synchronization
Figure 23.8 displays class
SimpleArray
with the proper synchronization. Notice that it's
identical to the
SimpleArray
class of Fig. 23.5, except that
add
is now a
synchronized
method (line 20). So, only one thread at a time can execute this method. We reuse classes
ArrayWriter
(Fig. 23.6) and
SharedArrayTest
(Fig. 23.7) from the previous example.