Table of Contents
- Item 78. Synchronize access to shared mutable data
- Item 79. Avoid excessive synchronization
- Item 80. Prefer executors, tasks, and streams to threads
- Item 81. Prefer concurrency utilities to wait and notify
- Item 82. Document thread safety
- Item 83. Use lazy initialization judiciously
- Item 84. Don’t depend on the thread scheduler
Item 78. Synchronize Access to Shared Mutable Data
Synchronize access to shared mutable data
If multiple threads share mutable data, reads and writes must be synchronized. Otherwise threads may observe unintended values.
Item 79. Avoid Excessive Synchronization
Avoid excessive synchronization
Lack of synchronization is harmful, but excessive synchronization is also harmful. It can reduce performance, cause deadlocks, and even produce unpredictable failures.
Item 80. Prefer Executors, Tasks, and Streams to Threads
Prefer executors, tasks, and streams to threads
You can manage threads directly, but using the concurrent package yields much simpler code.
Item 81. Prefer Concurrency Utilities to wait and notify
Prefer concurrency utilities to wait and notify
Use higher-level utilities instead of wait and notify.
Utilities in java.util.concurrent are broadly grouped into executor framework,
concurrent collections, and synchronizers.
Item 82. Document Thread Safety
Document thread safety
synchronized itself does not document thread-safety policy.
For conditionally thread-safe classes, clients need to know required call order and required external locks.
For unconditionally thread-safe classes, private lock objects are preferred.
Item 83. Use Lazy Initialization Judiciously
Use lazy initialization judiciously
In short: do not use it until necessary. Lazy initialization postpones field initialization until first use, usually for optimization. But depending on initialization cost, access patterns, and usage ratio, it can degrade performance instead of improving it. In most cases, normal eager initialization is better.
// Typical initialization for instance field
private final FieldType field = computeFieldValue();
If lazy initialization helps break initialization cycles, use a synchronized accessor.
private FieldType field;
private synchronized FieldType getField() {
if (field == null)
field = computeFieldValue();
return field;
}
For static field lazy initialization with performance concerns, use lazy holder class.
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
private static FieldType getField() {
return FieldHolder.field;
}
For instance field lazy initialization with performance concerns, use double-check idiom.
The local variable result helps ensure one field read when already initialized.
// Must be declared volatile
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result != null) // first check (no lock)
return result;
synchronized(This) {
if (field == null) // second check (with lock)
field = computeFieldValue();
return field;
}
}
If repeated initialization is acceptable, second check can be removed (single-check idiom).
// volatile is still required.
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result == null)
field = result = computeFieldValue();
return result;
}
For primitive field types other than long/double, even volatile may be removable in this single-check variant.
Item 84. Don’t Depend on the Thread Scheduler
Don’t depend on the thread scheduler
Do not depend on scheduler behavior
When multiple threads run, OS thread scheduler decides execution order. Policies differ by operating system. So application behavior should not rely on specific scheduling behavior. Otherwise performance differs by platform and portability suffers.
Characteristics of performant and portable programs
Average number of runnable threads should not significantly exceed number of processors. Then scheduler has less contention to resolve. Runnable threads should continue until assigned work completes.
To keep runnable thread count low, each thread should wait after finishing current work until new work arrives. A thread with no work should not stay runnable.
With executor frameworks, this means sizing thread pools properly and keeping tasks reasonably short. Too short tasks can also degrade performance.
Never keep threads in busy-wait state
Do not spin continuously checking shared state changes. Busy waiting is vulnerable to scheduler variance and wastes CPU, stealing execution opportunities from useful work.
public class SlowCountDownLatch {
private int count;
public SlowCountDownLatch(int count) {
if (count < 0)
throw new IllegalArgumentException(count + " < 0");
this.count = count;
}
public void await() {
while (true) {
synchronized(this) {
if (count == 0)
return;
}
}
}
public synchronized void countDown() {
if (count != 0)
count--;
}
}
This code is much slower than CountDownLatch from concurrent package.
When one or more threads stay runnable unnecessarily, performance and portability drop.
Thread.yield gives execution chance to other threads.
Even when a program seems to progress only because one thread rarely gets CPU time,
using yield is usually a bad fix.
It is hard to test reliably, and even if performance improves on one platform, portability can worsen. A better approach is redesigning structure to reduce simultaneous runnable threads. Changing thread priority is also risky and highly non-portable.