Skip to content

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.


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 state
class Counter {
private int count = 0;
public void increment() { count++; } // Not thread-safe
}
// Good: Immutable
final 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);
}
}
  • 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 synchronization
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
synchronized(map) {
if (!map.containsKey("key")) {
map.put("key", "value");
}
}
// Good: Higher-level utility
Map<String, String> map = new ConcurrentHashMap<>();
map.putIfAbsent("key", "value");
  • Document thread safety guarantees
  • Use final fields wherever possible
  • Follow consistent synchronization policies
  • Consider thread safety during code reviews

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);
}
}

Keep mutable data confined to a single thread.

// Thread confinement
ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd")
);
String format(Date date) {
return dateFormat.get().format(date);
}

Coordinate access to shared mutable state.

public class Counter {
private long count = 0;
public synchronized void increment() {
count++;
}
public synchronized long getCount() {
return count;
}
}

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();
}
}

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; }
}

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 locking
public 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 locking
public 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();
}
}
}
  • 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 cores
int cpuBoundPoolSize = Runtime.getRuntime().availableProcessors();
ExecutorService cpuBoundPool = Executors.newFixedThreadPool(cpuBoundPoolSize);
// I/O-bound tasks: Use more threads
int ioBoundPoolSize = Runtime.getRuntime().availableProcessors() * 2;
ExecutorService ioBoundPool = Executors.newFixedThreadPool(ioBoundPoolSize);
  • Batch related operations
  • Use work stealing (ForkJoinPool)
  • Avoid thread starvation
  • Choose the right concurrent collection
  • Consider memory layout and cache effects
  • Use value types (Java 10+) when available
// Choose the right collection for the workload
Map<String, String> map;
// High read, low write -> ConcurrentHashMap
map = new ConcurrentHashMap<>();
// Need sorting -> ConcurrentSkipListMap
map = new ConcurrentSkipListMap<>();
// Single writer, multiple readers -> CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();

// 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 implementation
private 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 idiom
public class HelperHolder {
private static class Holder {
static final Helper INSTANCE = new Helper();
}
public static Helper getHelper() {
return Holder.INSTANCE;
}
}
// Deadlock risk
synchronized(lockA) {
synchronized(lockB) {
// Work with both resources
}
}
// Better: Consistent lock ordering
private 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
}
}
}
// Bad: Holding lock during expensive operation
synchronized void processData(Data data) {
// Expensive computation with data
expensiveOperation(data);
}
// Better: Minimize synchronized block
void 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);
}
// Bad: Swallowing interruption
void sleepBadly() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Do nothing - BAD!
}
}
// Good: Propagate or restore interrupt
void sleepWell() throws InterruptedException {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Restore flag
throw e; // Propagate
}
}

// Simple stress test
void 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;
}
  • JCStress
  • Java Concurrency Stress tests
  • TestNG parallel testing
// Inject random delays to expose race conditions
void randomDelay() {
try {
Thread.sleep(new Random().nextInt(10));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

// Generate thread dump
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(true, true);
for (ThreadInfo info : threadInfos) {
System.out.println(info);
}
// Detect deadlocks
ThreadMXBean 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);
}
}
// Log thread operations
void logThreadOperation(String operation) {
Logger.getLogger("concurrency").info(
String.format("[%s] %s",
Thread.currentThread().getName(),
operation)
);
}

// 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;
}
}
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();
}
}
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;
}
}