Best Practices and Performance
Writing correct, efficient, and maintainable concurrent code requires careful consideration of design patterns, potential pitfalls, and performance implications. This guide covers essential best practices and performance optimization techniques for Java concurrency.
Fundamental Principles
Section titled “Fundamental Principles”Minimize Shared Mutable State
Section titled “Minimize Shared Mutable State”Shared mutable state is the root of most concurrency problems. Minimize it by:
- Using immutable objects
- Isolating mutable state
- Passing copies rather than references
- Using thread-local variables
// Bad: Shared mutable stateclass Counter { private int count = 0; public void increment() { count++; } // Not thread-safe}
// Good: Immutablefinal class ImmutablePoint { private final int x; private final int y;
public ImmutablePoint(int x, int y) { this.x = x; this.y = y; }
public int getX() { return x; } public int getY() { return y; }
// Create new instance for "modifications" public ImmutablePoint translate(int dx, int dy) { return new ImmutablePoint(x + dx, y + dy); }}Prefer Higher-Level Concurrency Utilities
Section titled “Prefer Higher-Level Concurrency Utilities”- Use concurrent collections instead of synchronized collections
- Use Executors instead of raw threads
- Use atomic variables instead of synchronized for counters
- Use CountDownLatch, CyclicBarrier, etc. instead of wait/notify
// Bad: Low-level synchronizationMap<String, String> map = Collections.synchronizedMap(new HashMap<>());synchronized(map) { if (!map.containsKey("key")) { map.put("key", "value"); }}
// Good: Higher-level utilityMap<String, String> map = new ConcurrentHashMap<>();map.putIfAbsent("key", "value");Design for Thread Safety from the Start
Section titled “Design for Thread Safety from the Start”- Document thread safety guarantees
- Use final fields wherever possible
- Follow consistent synchronization policies
- Consider thread safety during code reviews
Thread Safety Strategies
Section titled “Thread Safety Strategies”1. Immutability
Section titled “1. Immutability”Immutable objects are inherently thread-safe.
public final class Complex { private final double re; private final double im;
public Complex(double re, double im) { this.re = re; this.im = im; }
public double realPart() { return re; } public double imaginaryPart() { return im; }
public Complex plus(Complex c) { return new Complex(re + c.re, im + c.im); }
public Complex minus(Complex c) { return new Complex(re - c.re, im - c.im); }}2. Confinement
Section titled “2. Confinement”Keep mutable data confined to a single thread.
// Thread confinementThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial( () -> new SimpleDateFormat("yyyy-MM-dd"));
String format(Date date) { return dateFormat.get().format(date);}3. Synchronization
Section titled “3. Synchronization”Coordinate access to shared mutable state.
public class Counter { private long count = 0;
public synchronized void increment() { count++; }
public synchronized long getCount() { return count; }}4. Atomic Variables
Section titled “4. Atomic Variables”Use atomic variables for simple state that can be updated atomically.
public class AtomicCounter { private final AtomicLong count = new AtomicLong(0);
public void increment() { count.incrementAndGet(); }
public long getCount() { return count.get(); }}5. Delegation
Section titled “5. Delegation”Delegate thread safety to existing thread-safe classes.
public class DelegatingVehicleTracker { private final ConcurrentMap<String, Point> locations;
public DelegatingVehicleTracker(Map<String, Point> points) { locations = new ConcurrentHashMap<>(points); }
public Point getLocation(String id) { return locations.get(id); }
public void setLocation(String id, int x, int y) { locations.replace(id, new Point(x, y)); }
public Map<String, Point> getLocations() { return Collections.unmodifiableMap(new HashMap<>(locations)); }}
public class Point { private final int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int getX() { return x; } public int getY() { return y; }}Performance Optimization
Section titled “Performance Optimization”Reduce Contention
Section titled “Reduce Contention”Contention occurs when multiple threads compete for the same resource.
Strategies:
- Use fine-grained locking
- Reduce lock duration
- Use lock striping
- Use non-blocking algorithms
- Optimize for the common case
// Bad: Coarse-grained lockingpublic class CoarseList<E> { private final List<E> list = new ArrayList<>();
public synchronized void add(E e) { list.add(e); } public synchronized E get(int i) { return list.get(i); } public synchronized int size() { return list.size(); }}
// Better: Fine-grained lockingpublic class FineList<E> { private final List<E> list = new ArrayList<>(); private final ReadWriteLock lock = new ReentrantReadWriteLock();
public void add(E e) { lock.writeLock().lock(); try { list.add(e); } finally { lock.writeLock().unlock(); } }
public E get(int i) { lock.readLock().lock(); try { return list.get(i); } finally { lock.readLock().unlock(); } }
public int size() { lock.readLock().lock(); try { return list.size(); } finally { lock.readLock().unlock(); } }}Optimize Thread Pool Configuration
Section titled “Optimize Thread Pool Configuration”- Match pool size to workload characteristics
- Use different pools for different types of work
- Monitor and adjust pool sizes
// CPU-bound tasks: Use number of coresint cpuBoundPoolSize = Runtime.getRuntime().availableProcessors();ExecutorService cpuBoundPool = Executors.newFixedThreadPool(cpuBoundPoolSize);
// I/O-bound tasks: Use more threadsint ioBoundPoolSize = Runtime.getRuntime().availableProcessors() * 2;ExecutorService ioBoundPool = Executors.newFixedThreadPool(ioBoundPoolSize);Reduce Context Switching
Section titled “Reduce Context Switching”- Batch related operations
- Use work stealing (ForkJoinPool)
- Avoid thread starvation
Use Efficient Data Structures
Section titled “Use Efficient Data Structures”- Choose the right concurrent collection
- Consider memory layout and cache effects
- Use value types (Java 10+) when available
// Choose the right collection for the workloadMap<String, String> map;
// High read, low write -> ConcurrentHashMapmap = new ConcurrentHashMap<>();
// Need sorting -> ConcurrentSkipListMapmap = new ConcurrentSkipListMap<>();
// Single writer, multiple readers -> CopyOnWriteArrayListList<String> list = new CopyOnWriteArrayList<>();Common Anti-Patterns and Pitfalls
Section titled “Common Anti-Patterns and Pitfalls”Double-Checked Locking (Incorrect)
Section titled “Double-Checked Locking (Incorrect)”// Incorrect implementation (pre-Java 5)private static Helper helper;public static Helper getHelper() { if (helper == null) { synchronized(HelperHolder.class) { if (helper == null) { helper = new Helper(); // Not safe without volatile } } } return helper;}
// Correct implementationprivate static volatile Helper helper;public static Helper getHelper() { if (helper == null) { synchronized(HelperHolder.class) { if (helper == null) { helper = new Helper(); } } } return helper;}
// Better: Use holder class idiompublic class HelperHolder { private static class Holder { static final Helper INSTANCE = new Helper(); }
public static Helper getHelper() { return Holder.INSTANCE; }}Nested Locks (Deadlock Risk)
Section titled “Nested Locks (Deadlock Risk)”// Deadlock risksynchronized(lockA) { synchronized(lockB) { // Work with both resources }}
// Better: Consistent lock orderingprivate final Object lockA = new Object();private final Object lockB = new Object();
void method1() { synchronized(lockA) { synchronized(lockB) { // Work with both resources } }}
void method2() { synchronized(lockA) { // Same order as method1 synchronized(lockB) { // Work with both resources } }}Excessive Synchronization
Section titled “Excessive Synchronization”// Bad: Holding lock during expensive operationsynchronized void processData(Data data) { // Expensive computation with data expensiveOperation(data);}
// Better: Minimize synchronized blockvoid processData(Data data) { // Make a copy while holding lock Data copy; synchronized(this) { copy = new Data(data); } // Process the copy without holding lock expensiveOperation(copy);}Ignoring Interruption
Section titled “Ignoring Interruption”// Bad: Swallowing interruptionvoid sleepBadly() { try { Thread.sleep(1000); } catch (InterruptedException e) { // Do nothing - BAD! }}
// Good: Propagate or restore interruptvoid sleepWell() throws InterruptedException { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Restore flag throw e; // Propagate }}Testing Concurrent Code
Section titled “Testing Concurrent Code”Use Stress Testing
Section titled “Use Stress Testing”// Simple stress testvoid stressTest() throws InterruptedException { final int threadCount = 10; final int iterationsPerThread = 1000; final Counter counter = new Counter();
Thread[] threads = new Thread[threadCount]; for (int i = 0; i < threadCount; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < iterationsPerThread; j++) { counter.increment(); } }); }
for (Thread t : threads) t.start(); for (Thread t : threads) t.join();
assert counter.getCount() == threadCount * iterationsPerThread;}Use Concurrency Testing Libraries
Section titled “Use Concurrency Testing Libraries”- JCStress
- Java Concurrency Stress tests
- TestNG parallel testing
Inject Timing Variations
Section titled “Inject Timing Variations”// Inject random delays to expose race conditionsvoid randomDelay() { try { Thread.sleep(new Random().nextInt(10)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }}Monitoring and Debugging
Section titled “Monitoring and Debugging”Thread Dumps
Section titled “Thread Dumps”// Generate thread dumpThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(true, true);for (ThreadInfo info : threadInfos) { System.out.println(info);}Deadlock Detection
Section titled “Deadlock Detection”// Detect deadlocksThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();if (deadlockedThreads != null) { ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads); System.out.println("Deadlocked threads:"); for (ThreadInfo info : threadInfos) { System.out.println(info); }}Logging and Metrics
Section titled “Logging and Metrics”// Log thread operationsvoid logThreadOperation(String operation) { Logger.getLogger("concurrency").info( String.format("[%s] %s", Thread.currentThread().getName(), operation) );}Code Examples
Section titled “Code Examples”Thread-Safe Lazy Initialization
Section titled “Thread-Safe Lazy Initialization”// Double-checked locking (Java 5+)class LazyInitialization { private volatile ExpensiveObject instance;
public ExpensiveObject getInstance() { if (instance == null) { synchronized(this) { if (instance == null) { instance = new ExpensiveObject(); } } } return instance; }}
// Holder class idiom (preferred)class LazyInitializationHolder { private static class Holder { static final ExpensiveObject INSTANCE = new ExpensiveObject(); }
public static ExpensiveObject getInstance() { return Holder.INSTANCE; }}Non-Blocking Algorithm Example
Section titled “Non-Blocking Algorithm Example”class NonBlockingCounter { private final AtomicInteger value = new AtomicInteger(0);
public int increment() { int current; int next; do { current = value.get(); next = current + 1; } while (!value.compareAndSet(current, next)); return next; }
public int get() { return value.get(); }}Thread-Safe Cache
Section titled “Thread-Safe Cache”class Memoizer<A, V> { private final ConcurrentMap<A, Future<V>> cache = new ConcurrentHashMap<>(); private final Computable<A, V> computable;
public Memoizer(Computable<A, V> computable) { this.computable = computable; }
public V compute(A arg) throws InterruptedException, ExecutionException { while (true) { Future<V> future = cache.get(arg); if (future == null) { Callable<V> eval = () -> computable.compute(arg); FutureTask<V> futureTask = new FutureTask<>(eval); future = cache.putIfAbsent(arg, futureTask); if (future == null) { future = futureTask; futureTask.run(); } } try { return future.get(); } catch (CancellationException e) { cache.remove(arg, future); } } }
interface Computable<A, V> { V compute(A arg) throws InterruptedException; }}