Chapter 2. Thread Safety
- Writing thread safe code is, at its core, about managing access to shared, mutable state.
state
:- an object's state is its data, stored in state variables such as instance or static fields.
- An object's state may include fields from other, dependent objects;
- An object's state encompasses any data that can affect its externally visible behavior.
shared
:- variable could be accessed by multiple threads
mutable
:- its value could change during its lifetime.
- Whether an object needs to be thread safe is a property of how the object is used in a program, not what it does.
- Making an object thread safe requires using synchronization to coordinate access to its mutable state
If multiple threads access the same mutable state variable without appropriate synchronization, your program is broken. There are three ways to fix it:
- Don't share the state variable across threads;
- Make the state variable immutable; or
- Use synchronization whenever accessing the state variable.
When designing thread safe classes, good object oriented techniques encapsulation, immutability, and clear specification of invariants are your best friends.
- The less code that has access to a particular variable, the easier it is to ensure that all of it uses the proper synchronization, and the easier it is to reason about the conditions under which a given variable might be accessed.
pursue optimization only if your performance measurements and requirements tell you that you must, and if those same measurements tell you that your optimizations actually made a difference under realistic conditions.
- a program that consists entirely of thread safe classes may not be thread safe
- the concept of a thread safe class makes sense only if the class encapsulates its own state.
- Thread safety may be a term that is applied to code, but it is about state, and it can only be applied to the entire body of code that encapsulates its state.
2.1 What is Thread Safety?
- A class is thread safe if it behaves correctly when accessed from multiple threads, regardless of the scheduling or interleaving of the execution of those threads by the runtime environment, and with no additional synchronization or other coordination on the part of the calling code.
- Thread safe classes encapsulate any needed synchronization so that clients need not provide their own.
2.2 Atomicity
Race Condition and Data Race
Race condition
occurs when the correctness of a computation depends on the relative timing or interleaving of multiple threads by the runtime; in other words, when getting the right answer relies on lucky timing.Data race
is happened when synchronization is not used to coordinate all access to a shared non final field. You risk a data race whenever a thread writes a variable that might next be read by another thread or reads a variable that might have last been written by another thread if both threads do not use synchronization
Compound Actions
atomic
: Operations A and B are atomic with respect to each other if, from the perspective of a thread executing A, when another thread executes B, either all of B has executed or none of it has.- An atomic operation is one that is atomic with respect to all operations, including itself, that operate on the same state.
Common Pattern that Has Synchronized Problem
read modify write
- Ex: Increment
- the resulting state is derived from the previous state.
check then act
- Ex: Lazy Initialization
- where a potentially stale observation is used to make a decision on what to do next); but in fact the observation could have become invalid between the time you observed it and the time you acted on it.
put if absent
2.3 Locking/Synchronized
- To preserve state consistency, update related state variables in a single atomic operation.
- Java built in locking mechanism for enforcing atomicity: the
synchronized
block. - A synchronized block has two parts:
- a reference to an object that will serve as the lock
- a block of code to be guarded by that lock.
- Every Java object can implicitly act as a lock for purposes of synchronization
- The lock is automatically acquired by the executing thread before entering a synchronized block and automatically released when control exits the synchronized block, whether by the normal control path or by throwing an exception out of the block.
- The only way to acquire an intrinsic lock is to enter a synchronized block or method guarded by that lock.
- Intrinsic locks in Java act as mutexes (or mutual exclusion locks), which means that at most one thread may own the lock.
- When thread A attempts to acquire a lock held by thread B, A must wait, or block, until B releases it. If B never releases the lock, A waits forever.
Reentrancy
locks are acquired on a per thread basis rather than per invocation basis.
- Reentrancy is implemented by associating with each lock an acquisition count and an owning thread.
- When the count is zero, the lock is considered unheld.
- When a thread acquires a previously unheld lock, count set to one.
- If that same thread acquires the lock again, the count is incremented
- When the owning thread exits the synchronized block, the count is decremented.
- When the count reaches zero, the lock is released.
- Reentrancy is implemented by associating with each lock an acquisition count and an owning thread.
2.4. Guarding State with Locks
- For each mutable state variable that may be accessed by more than one thread, all accesses to that variable must be performed with the same lock held. In this case, we say that the variable is guarded by that lock.
- Acquiring the lock associated with an object does not prevent other threads from accessing that object; the only thing that acquiring a lock prevents any other thread from doing is acquiring that same lock.
- Every shared, mutable variable should be guarded by exactly one lock. Make it clear to maintainers which lock that is.
- For every invariant that involves more than one variable, all the variables involved in that invariant must be guarded by the same lock.
2.5 Liveness and Performance
- There is frequently a tension between simplicity and performance. When implementing a synchronization policy, resist the temptation to prematurely sacrifice simplicity (potentially compromising safety) for the sake of performance.
- By narrowing the scope of the synchronized block, it is easy to improve the performance while maintaining thread safety.
- Acquiring and releasing a lock has some overhead, so it is undesirable to break down synchronized blocks too far.
- Avoid holding locks during lengthy computations or operations at risk of not completing quickly such as network or console I/O.