Table of Contents


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.

When multiple threads share mutable data, synchronize both read and write operations.



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.

Never hand control to clients inside synchronized methods or blocks.



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.

Use the executor framework instead of managing threads directly.



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.

Use concurrency utilities instead of wait/notify methods.



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.

Document thread-safety characteristics explicitly.



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.

Lazy initialization can degrade performance in some cases.



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.

Do not rely on thread scheduler behavior for program correctness.