Concurrency Best Practices

Rules That Prevent Most Bugs

  1. Prefer immutability: records, List.copyOf, final fields. Immutable data needs no locks.
  2. Do not share mutable state; if you must, keep it inside one class with all access synchronized.
  3. Use high-level tools: executors, CompletableFuture, ConcurrentHashMap, atomics, BlockingQueue — not raw Thread and wait/notify.
  4. Size pools by workload: CPU-bound ≈ number of cores; I/O-bound → virtual threads (Java 21) or larger bounded pools.
  5. Always use bounded queues and timeouts for remote calls; never wait forever.
  6. Lock in a fixed order, keep critical sections short, never call unknown code (callbacks) while holding a lock.
  7. Handle InterruptedException: restore the flag with Thread.currentThread().interrupt() or rethrow.
  8. Name your threads (Thread.ofPlatform().name("payment-", 0)) — thread dumps become readable.

Check-then-act on a Shared Map

Example

if (!map.containsKey(k)) map.put(k, v) is a race even on a ConcurrentHashMap. computeIfAbsent is atomic: the expensive load runs once per key.

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class Main {
    public static void main(String[] args) {
        ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
        AtomicInteger loads = new AtomicInteger();

        try (ExecutorService pool = Executors.newFixedThreadPool(8)) {
            for (int i = 0; i < 1000; i++) {
                String key = "product-" + (i % 5);
                pool.submit(() -> cache.computeIfAbsent(key, k -> {
                    loads.incrementAndGet();          // expensive DB call happens here
                    return k.toUpperCase();
                }));
            }
        }
        System.out.println("Cache: " + new TreeMap<>(cache));
        System.out.println("DB loads: " + loads.get() + " (one per key)");
    }
}

Output

Cache: {product-0=PRODUCT-0, product-1=PRODUCT-1, product-2=PRODUCT-2, product-3=PRODUCT-3, product-4=PRODUCT-4}
DB loads: 5 (one per key)