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.

Wednesday, September 7, 2016

Java Concurrency in Practice - Chapter 1 - Introduction

Benefits of threads:

  • Exploiting multiple processors.

    • When properly designed, multithreaded programs can improve throughput by utilizing available processor resources more effectively.
    • On single processor systems, while one thread is blocked/wait for a synchronous event such as I/O operation to complete, the another thread can still run, allow the application make progress.

  • Simplicity of modeling.

    • A program that processes one type of task sequentially is simpler to write, less error-prone, and easier to test than one managing multiple different types of tasks at once. With multi threads available, a complicated, asynchronous workflow can be decomposed into a number of simpler, synchronous workflows each running in a separate thread.

  • Simplified handling of asynchronous events.

    • In a single-threaded application, an I/O block could stall the whole processing. To avoid this problem, single-threaded applications are forced to use non-blocking I/O, which is far more complicated and error-prone than synchronous I/O.

  • More responsive user interfaces.

    • If the code called from the main event loop takes too long to execute, the user interface appears to "freeze" until that code finishes, because subsequent user interface events cannot be processed until control is returned to the main event thread. By assigning the long-running task to a separate thread, the main event thread remains free to process UI events, making UI more responsive.

Risks of threads

  • Safety hazards

    • Safety = nothing bad ever happens.
    • Example: race condition.
    • In the absence of sufficient synchronization, the ordering of operations in multiple threads is unpredictable and sometimes surprising. 

  • Liveness hazards

    • Liveness = something good eventually happens.
    • Example: deadlock, starvation, livelock.
    • A liveness failure occurs when an activity gets into a state such that it is permanently unable to make forward progress.

  • Performance hazards

    • Performance = good thing to happen quickly.
    • Example: poor service time, responsiveness, throughput, resource consumption, scalability.
    • Context switches: when the scheduler suspends the active thread temporarily so another thread can run. The higher number of threads, the higher significant costs for saving and restoring execution context, loss of locality, and CPU time spent scheduling threads instead of running them.
    • The synchronization of shared data can inhibit compiler optimizations, flush or invalidate memory caches, and create synchronization traffic on the shared memory bus.