Java Concurrency in Practice
Visibility [3.1]
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
Possible results:
- method prints
42
(expected), - method never returns (value of
ready
is not visible to another thread - stale data), - method prints
0
(assigning tonumber
andready
is reordered).
When a thread reads a variable without synchronization, it may see a stale value, but at least it sees a value that was actually placed there by some thread rather than some random value. This safety guarantee is called out-of-thin-air safety.
Locking is not just about mutual exclusion; it is also about memory visibility. To ensure that all threads see the most up-to-date values of shared mutable variables, the reading and writing threads must synchronize on a common lock.
Locking can guarantee both visibility and atomicity; volatile variables can only guarantee visibility.
Immutability
An object is immutable if:
- Its state cannot be modified after construction,
- All its fields are
final
, - It is properly constructed (the this reference does not escape during construction).
Safe Publication Idioms
A properly constructed object can be safely published by:
- initializing an object reference from a static initializer [JLS 12.4.2],
- storing a reference to it into a
volatile
field orAtomicReference
, - storing a reference to it into a final field of a properly constructed object,
- storing a reference to it into a field that is properly guarded by a lock.
Publication requirements for objects:
- immutable objects can be published through any mechanism,
- effectively immutable objects (do not change after publication) must be safely published,
- mutable objects must be safely published and must be either thread-safe or guarded by a lock.
Policies for using and sharing objects in concurrent programs:
- thread-confined object is owned exclusively by and confined to one thread, and can be modified by its owning thread,
- shared read-only object can be accessed concurrently by without additional synchronization, but cannot be modified by any thread. Shared read-only objects include immutable and effectively immutable objects,
- shared thread-safe object performs synchronization internally, so multiple threads can freely access it through its public interface without further synchronization,
- guarded object can be accessed only with a specific lock held. Guarded objects include those that are encapsulated within other thread-safe objects and published objects that are known to be guarded by a specific lock.
Object can be instance confined - encapsulated within another object so that all code paths are known. Confinement makes it easier to build thread-safe classes because a class that confines its state can be analyzed for thread safety without having to examine the whole program.
Designing thread-safe classes
Design process:
- identify object's state variables,
- identify object invariants that constrain the state variables,
- establish policy for managing concurrent access to object's state. - the policy defines how objects coordinate access to object's state without violating object's invariants or postconditions. It is a combination of immutability, thread confinement and locking.
You cannot ensure thread safety without understanding an object's invariants and postconditions. Constraints on the valid values or state transitions for state variables can create atomicity and encapsulation requirements.
State ownership
Ownership and encapsulation go together - object encapsulates the state that it owns and owns the state it encapsulates. A class usually does not own the objects passed to its methods or constructors, unless the method is designed to explicitly transfer ownership of objects passed in (such as the synchronized collection wrapper factory methods).
If a class is composed of multiple independent thread-safe state variables and has no operations that have any invalid state transitions, then it can delegate thread safety to the underlying state variables.
Concurrent collections
The producer-consumer pattern also enables several performance benefits. Producers and consumers can execute concurrently; if one is I/O-bound and the other is CPU-bound, executing them concurrently yields better overall throughput than executing them sequentially. If the producer and consumer activities are parallelizable to different degrees, tightly coupling them reduces parallelizability to that of the less parallelizable activity.
Producer-consumer pattern
The iterators returned by ConcurrentHashMap are weakly consistent instead of fail-fast. A weakly consistent iterator can tolerate concurrent modification, traverses elements as they existed when the iterator was constructed, and may (but is not guaranteed to) reflect modifications to the collection after the construction of the iterator.
Synchronizers
A synchronizer is any object that coordinates the control flow of threads based on its state. Blocking queues can act as synchronizers; other types of synchronizers include semaphores, barriers, and latches.
- latch is a synchronizer that can delay the progress of threads until it reaches its terminal state,
- counting semaphores are used to control the number of activities that can access a certain resource or perform a given action at the same time; a degenerate case of a counting semaphore is a binary semaphore, a Semaphore with an initial count of one. A binary semaphore can be used as a mutex with non-reentrant locking semantics; whoever holds the sole permit holds the mutex,
- barriers are similar to latches in that they block a group of threads until some event has occurred, the key difference is that with a barrier, all the threads must come together at a barrier point at the same time in order to proceed. Another form of barrier is
Exchanger
, a two-party barrier in which the parties exchange data at the barrier point.Exchangers
are useful when the parties perform asymmetric activities, for example when one thread fills a buffer with data and the other thread consumes the data from the buffer; these threads could use anExchanger
to meet and exchange a full buffer for an empty one.
Summary of Part I
- It's the mutable state, stupid. All concurrency issues boil down to coordinating access to mutable state. The less mutable state, the easier it is to ensure thread safety.
- Make fields
final
unless they need to be mutable. - Immutable objects are automatically thread-safe.
- Immutable objects simplify concurrent programming tremendously. They are simpler and safer, and can be shared freely without locking or defensive copying.
- Encapsulation makes it practical to manage the complexity.
- You could write a thread-safe program with all data stored in global variables, but why would you want to? Encapsulating data within objects makes it easier to preserve their invariants; encapsulating synchronization within objects makes it easier to comply with their synchronization policy.
- Guard each mutable variable with a lock.
- Guard all variables in an invariant with the same lock.
- Hold locks for the duration of compound actions.
- A program that accesses a mutable variable from multiple threads without synchronization is a broken program.
- Don't rely on clever reasoning about why you don't need to synchronize.
- Include thread safety in the design process or explicitly document that your class is not thread-safe.
- Document your synchronization policy.
[Chapter 8.3 ]