Chapter 6. Task Execution Drawbacks of creating unlimited number of threads: Thread lifecycle overhead – thread creation and teardown overhead Resource consumption. Platform / JVM dependent limit: stack ~ outOfMemoryError. Thread Pools:
- newScheduledThreadPool – timer
ExecutorService Interface extends Executor, adding methods for lifecycle management. Runnable -> Callable -> Future -> completionQueue Runnable has limited side effect: writing to a log or a shared data structure Callable expects to return a value or exception Lifecycle of a task to Executor: created, submitted, started, and completed. Future provides methods to test whether the task’s state and get the result or exception. completionQueue has results queued up in a QueueingFuture, a subclass of FutureTask, that place the results on a BlockingQueue. inVolkeAll can submit a list of tasks to executor. Chapter 7. Cancellation and Shutdown Quote: “FutureTask and Executor simplifies building cancellable tasks and services.” Use a single volatile boolean: a blocking queue may never check the boolean of a while-check-loop. Calling interrupt does not guarantee the thread will stop. A guest task should preserve the executing thread’s interruption status. – propagate the exception making the method an interruptible blocking method – Restore the interruption status so that code higher up on the call stack can deal with it Cancel through Future. Should not manipulate a thread unless you own it. JVM Shutdown shutdown hooks normal threads v.s. daemon threads – daemon threads do not prevent the JVM from shutting down. Finalisers are only useful if managing objects holding resources acquired by native methods. Chapter 8. Applying Thread Pools limited thread pool faces potential starvation, where a job is dependent on 2 threads it created and the later two are queued after this thread. Long-running tasks. Bounded queue, unbounded queue and synchronous handoff. Saturation policies deal with a bounded filled workk queue. ThreadPoolExecutor is easily extended by defining beforeExecute and afterExecute hooks. Puzzel Solver Framework from page 114 is a comprehensive example. Chapter 10. Avoid Liveness Hazards Deadlock Acquiring multiple locks introduces deadlock hazard. The order of acquiring locks should be the same for each method. This hazards are not always obvious: Dynamic lock order deadlocks (withdraw and lodge simultaneously: A-> B and B->A); Deadlock between cooperating objects. Open call: Prefer syncronized blocks to synchronised methods. Quote: “Programs that rely on open calls are easier to analyse than that calls alien methods with locks held.” Resource deadlock: Quote: “bounded pools and interdependent tasks do not mix well.” Because this causes thread starvation while a thread is waiting for a long running computation to finish. Lock ordering is a part of design when a program has to acquire multiple locks. Prefer timed trylock, i.e. explicit locks to intrinsic lock. Starvation Avoid to change thread priorities, which introduces platform dependency and possibly starvation, where a thread perpetually yield to other threads. Poor Responsiveness Lower priority of computational expensive background threads to make foreground thread more responsive. This can also be caused by poor lock management, for example, a thread takes to long to iterate a locked collection, while others are blocked and unresponsive. Livelock A not-blocked thread makes no process and keeps trying and failing. This can also happen when multiple threads are changing their state in response to the others so that nobody makes progress. Solution: introduce randomness into the retry mechanism. Chapter 11: Performance and Scalability Performance costs introduced by multi-threading: locking, signalling, context switch, memory synchronisation, thread creation and teardown, scheduling overhead. Context switch – JVM switch out blocked thread. Memory synchronisation – visibility guarantee may involve memory barriers that flush and invalidate caches. Blocking – uncontended synchronisation can be optimised by JVM; contended synchronisation requires OS actively, context switch, etc. Performance – how fast ; how much Scalability – the ability to improve performance when additional resources are added. Always measure before optimisation – perfbar Reduce lock contention to improve performance and scalability Serialisation hurts scalability; context switch hurts performance; contended lock hurts both. Approaches – 1. reduce the duration of lock 2. reduce frequency of lock requests 3. replace exclusive locks with coordination mechanisms – e.g. centralised logging with a queue v.s. each thread contend for the shared I/O stream.. Allocating new objects is usually cheaper than synchronising. Chapter 12. Testing Concurrent Programs Safety “Nothing bad ever happens” | Liveness “Something good eventually happens ” Introduce More Inter-leavings:
- Creates more threads than the number of cores
- Use Thread.yield to bring more context switches
- Testing on variety of systems with different processor counts, OS, processor architecture
Adding calls for testing and removing them for production can be implemented using aspect-oriented programming tools. Run benchmark after the code is “warm up”, when JIT decides to compile byte code instead of interpreting the byte code Use -server instead of -client for both test and production code Avoid dead code in thread-local computation, print the values in case compiler optimise them away | better: compare the hashCode of that object to an arbitrary value and print only if the two hashCodes match QA methodologies:
- Code Review
- Static Analysis Tools
- Aspect-oriented Testing Techniques
- Profilers and Monitoring Tools
Chapter 13 – Explicit Locks ReentrantLock v.s. IntrinsicLock：fail gracefully and remain responsive by using timer (lock.tryLock(nanosToLock, unit)) or interruptible lock (lock.lockInterruptibly()) Advanced features of reentrantlock: timed, polled (tryLock), interruptible lock acquisition, fair queuing, non-block-structured locking (non-blocking linked list implementation makes use of the last one). Choose intrinsic lock if: do not need the advanced features of reentrantlock; intrinsic locking information is included in thread dumps with call frames; synchronised block allows JVM optimisation. Fair lock performs worse then unfair lock. For example, B is suspended waiting for A’s lock, C may instantly get the lock before B wakes up and finish its work before B completely wakes up. Unfair lock works better in this scenario. JVM is not required to implement fair lock for intrinsic lock. Always unlock reentrantlock in Finally block. Read-write lock: can access by multiple readers or a single write at the same time, but not both. Chapter 14 Building Custom Synchroniser: to build a state-dependent class Condition queue describes how multiple threads waiting on the same object’s notification. Condition has to be checked each time the thread is notified; remember to notify while holding the lock (notify in synchronised block or method); almost always use notifyAll instead of notify (use notify only uniform waiters with one condition predicate + one-on one-out). Issues with condition queue: subclass safety – document the notify protocol or make the base class final to prevent subclassing; encapsulate condition queue so alien code cannot wait on it. Along with single condition queue for intrinsic lock, Explicit Conditions exist for explicit lock.
- wait – await
- notify – signal
- notifyAll – signalAll
Quote “Condition offers a richer feature set than intrinsic condition queues: multiple waitsets per lock,interruptible and uninterruptible condition waits,deadline based waiting, and a choice of fair or nonfair queueing. “ ReentrantLock, semaphore, reentrantReadWriteLock, countDownLatch, synchronousQueue and FutureTask are built using AQS – abstract queued synchroniser. AQS holds a state that can be used to implement different concurrent mechanisms. Chapter 15 Atomic Variables and Non-blocking Algorithm compare-and-swap CAS Non-blocking Algorithms: can show starvation or live lock. To implement ConcurrentLinkedList using CAS is a good example. ABA problem: use AtomicStampedReference(and its cousin AtomicMarkableReference) to address this because they update a pair of values, a reference and a version number.