This is part 9 of my notes from reading Java Concurrency in Practice.
Chapter 13 - Explicit Locks
- Unlike intrinsic locking, the Lock interface offers unconditional, polled, timed, and interruptible lock acquisition.
- Lock implementations provide the same memory visibility guarantees as intrinsic locking. They can vary in locking semantics, scheduling algorithms, ordering guarantees and performance.
- ReentrantLock has same semantics as a synchronized block.
- Why use explicit locks over intrinsic locks?
- Unlike intrinsic locking, a thread waiting to acquire a ReentrantLock can be interrupted.
- ReentrantLock also supports timed lock acquisition.
- WIth intrinsic locks, a deadlock is fatal.
- Intrinsic locks must be released in the same code block they are acquired in. This makes non-blocking designs impossible.
- ReentrantLock is much faster than intrinsic locking in Java 5.0
- Lock objects are usually released in a finally block, to make sure that it is released if an exception is thrown.
- lockInterruptibly() helps us build cancelable tasks.
- tryLock() returns false if the lock cannot be acquired. Timed tryLock() is also responsive to interruption.
- ReentrantLock offers two fairness options
- Fair - threads acquire locks in order of requesting.
- Non-fair (default) - thread can acquire lock if it is available at the time of the lock request, even if earlier threads are waiting. Non-fair locking is useful because it avoids the overhead of suspending/resuming a thread if the lock is available at time of the lock request.
- Fairness is usually not needed, and has a very high performance penalty (multiple orders of magnitude).
- Fair locks work best when they are held for a relatively long time or when the mean time between lock requests is large.
- When to use intrinsic locks?
- synchronized blocks have a more concise syntax. You can never forget to unlock a synchronized block.
- Use ReentrantLock only when advanced features like timed, polled, interruptible lock acquisition, fairness or non-block structured locking are needed.
- Harder to debug deadlock problems when using ReentrantLock because lock acquisition is not tied to a particular stack frame, and thus the stack dump is not very helpful.
- synchronized is likely to have more performance improvements in the future (eg: lock coarsening) as it is part of the Java language spec.
- Read-Write Lock - protected resource can be accessed by multiple readers or one writer at the same time.
- offers readLock() and writeLock() methods which return a Lock object that must be acquired before doing the respective operations.
- More complex implementation. Hence has lower performance except in read-heavy workloads.
- Lock can only be released by thread that acquired it.
Chapter 14 - Building Custom Synchronizers
- State-dependent classes - blocking operations can proceed only if state-precondition becomes true (for example, you cannot retrieve result of FutureTask if computation has not yet finished).
- Try to use existing state-dependent classes whenever possible.
- Condition queue - allows a group of threads (called wait set) to wait for a specific condition to become true.
- Intrinsic condition queues - Any java object can act as a condition queue via the Object.wait(), notify() and notifyAll() functions.
- Must hold intrinsic lock on an object before you can call wait(), notify() or notifyAll().
- Calling Object.wait() atomically releases lock and suspends the current thread. It reacquires the lock upon waking up, just before returning from the wait() function call. wait() blocks till thread is awakened by a notification, a specified timeout expires or the thread is interrupted.
- In order to use condition queues, we must first identify and document the pre-condition that makes an operation state-dependent. The state variables involved in the condition must be protected by the same lock object as the one we wait() on.
- A single intrinsic condition queue can be used with more than one condition predicate. This means that when a thread is awakened by a notifyAll, the condition it was waiting on need not be true. wait() can even return spuriously without any notify(). The condition can also become false by the time wait() reacquires the lock after waking up. Hence when waking up from wait(), the condition predicate must be tested again and we must go back to waiting if it is false. Hence, call wait() in a loop: synchronized(lockObj) { while(!conditionPredicate()) { lock.wait();} // object is in desired state now
- Notifications are not sticky - i.e. a thread won't know about notifications that occurred before it called wait().
- In order to call notify() or notifyAll() on an object, you must hold the intrinsic lock on that object. Unlike wait(), the lock is not automatically released. The lock must be manually released soon as none of the woken up threads can make progress without acquiring the lock.
- Use notifyAll() instead of notify(). If multiple threads are waiting on the same condition queue for different condition predicates, calling notify() instead of notifyAll() can lead to missed signals, as only the wrong thread may be woken up.
- However using notifyAll() can be very inefficient, as multiple threads are woken up and contend for the lock where only one of them can usually make progress.
- notify() can be used only if
- The same condition predicate is associated with the condition queue and each thread executes the same logic on returning from wait().
- A notification on the condition queue enables at most one thread to proceed.
- A bounded buffer implementation needs to call notify only when moving away from the empty state or full states. Such conditional notifications are efficient, but makes the code hard to get right. Hence, avoid unless necessary as an optimization.
- A state dependent class should either fully document its waiting/notification protocols to sub-classes or prevent sub-classes from participating in them at all.
- Encapsulate condition queue objects in order to avoid external code from incorrectly calling wait() or notify() on them. This often implies the usage of a private lock object instead of using the main object itself.
- Explicit Condition objects - Condition
- Each intrinsic lock can have only one associated condition queue. Hence multiple threads may wait on same condition queue for different condition predicates.
- A Condition is associated with a single Lock object. A Condition is created by calling Lock.newCondition(). You can create multiple Condition objects per Lock.
- Equivalents of wait(), notify() and notifyAll() for Condition are await(), signal() and signalAll(). Since Condition is an Object, wait() and notify() are also available. Do not confuse them.
- Explicit Condition objects make it easier to use signal() instead of signalAll().
- Synchronizers
- Both Semaphore and ReentrantLock extend AbstractQueuedSynchronzer (AQS) class.
- AQS is a framework for building locks and synchronizers.
- When using AQS, there is only one point of contention.
- Acquisition - state dependent operation that can block.
- Release - allows some threads blocked in acquire to proceed. Not-blocking
- AQS manages a single integer of state for the synchronizer class. It can be accessed with getState(), setState() and compareAndSetState() methods. The integer can represent arbitrary semantics. For example, FutureTask uses it to represent the state (running, completed, canceled) of the task. Semaphore uses it to track the number of permits remaining.
- Synchronizers track additional state variables themselves.
- Synchronizers override tryAcquire, tryRelease, isHeldExclusively, tryAcquireShared and tryReleaseShared. The acquire, release, etc methods of AQS call the appropriate try methods,
No comments:
Post a Comment