Saturday, September 10, 2016

Java Concurrency in Practice - Chapter 2 - Thread Safety

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.

Stateless objects are always thread-safe.

Atomicity

A 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.

Race condition due to common compound actions below:
  • read-modify-write: e.g. hit count incrementing
  • check-then-act: e.g. lazy initialization
To avoid race condition, compound actions need to be 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.

Locking

When multiple variables participate in an invariant, they are not independent: the value of one constrains the allowed value(s) of the others. Even if they are atomic references which individually thread-safe, they can't avoid the race condition. To preserve state consistency, update related state variables in a single atomic operation.

Intrinsic locks

Java built-in locking mechanism for enforcing atomicity. synchronized block.

A synchronized block has 2 parts:
  • a reference to an object that will serve as the lock; once a thread acquire this lock, other threads must wait, until the thread releases the lock.
  • a block of code to be guarded by that lock; appear to execute as a single, indivisible unit.
Every Java object can implicitly act as a lock for purposes of synchronization; these built-in locks are called intrinsic locks or monitor locks. Intrinsic locks in Java act as mutexes (or mutual exclusion locks)

The synchronized block can easily make a class thread-safe. Using it carelessly, such as synchronizing the entire method can cause threads congestion which leads to another serious problem, which is the performance problem.

Intrinsic locks are reentrant. If a thread tries to acquire a lock that already holds, the request succeeds. Reentrancy saves us from the deadlock in the situation where a subclass overrides the synchronized method (e.g. doSomething()) of its parent class, then in overridden method subclass calls super.doSomething() method.

Guarding state with locks

For each mutable state variable that may be accessed by multiple threads, 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.

Every shared, mutable variable should be guarded by exactly one lock. Make it clear to maintainers which lock that is. The fact is any Java object can be the lock object besides the intrinsic lock which is just a convenience so that you need not explicitly create lock objects.

For every invariant that involves more than one variable, all the variables involved in that invariant must be guarded by the same lock.

Liveness and performance

Too much of synchronization can lead to liveness or performance problems such as poor concurrency and poor responsiveness.

Improve concurrency while maintaining thread safety by narrowing the scope of the synchronized block. Not too small; you would not want to divide an operation that should be atomic into more than one synchronized block. But it is reasonable to try to exclude from synchronized blocks long-running operations that do not affect shared states, so that other threads are not prevented from accessing the shared state while the long-running operation is in progress.

Deciding how big or small to make synchronized blocks may require tradeoffs among competing design forces, including safety (which must not be compromised), simplicity, and performance. Resist the temptation to prematurely sacrifice simplicity (potentially compromising safety) for the sake of performance. A reasonable balance can usually be found.

Avoid holding locks during lengthy computations or operations at risk of not completing quickly such as the network or console I/O.

No comments: