Careeroza
Concurrency Best Practices
Rules That Prevent Most Bugs
- Prefer immutability: records,
List.copyOf, final fields. Immutable data needs no locks. - Do not share mutable state; if you must, keep it inside one class with all access synchronized.
- Use high-level tools: executors,
CompletableFuture,ConcurrentHashMap, atomics,BlockingQueue— not rawThreadandwait/notify. - Size pools by workload: CPU-bound ≈ number of cores; I/O-bound → virtual threads (Java 21) or larger bounded pools.
- Always use bounded queues and timeouts for remote calls; never wait forever.
- Lock in a fixed order, keep critical sections short, never call unknown code (callbacks) while holding a lock.
- Handle
InterruptedException: restore the flag withThread.currentThread().interrupt()or rethrow. - 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)