Skip to content
Java advanced 4 min read

Concurrency

java.util.concurrent (JSR-166) provides high-level building blocks that make concurrent code correct and maintainable without hand-rolling threads and locks. Instead of managing Thread objects directly, you submit tasks to executors, compose asynchronous results, and reach for purpose-built thread-safe data structures.

ExecutorService and Thread Pools

An ExecutorService decouples task submission from execution. You submit work; a pool of reusable threads runs it. This avoids the cost of creating a thread per task and bounds resource usage.

import java.util.concurrent.*;

ExecutorService pool = Executors.newFixedThreadPool(4);

pool.submit(() -> System.out.println("task on " + Thread.currentThread().getName()));

pool.shutdown(); // stop accepting tasks; finish queued ones
pool.awaitTermination(5, TimeUnit.SECONDS);
FactoryPool behavior
newFixedThreadPool(n)Fixed n threads, unbounded queue
newCachedThreadPool()Grows on demand, reuses idle threads
newSingleThreadExecutor()One thread, serial execution
newVirtualThreadPerTaskExecutor()One virtual thread per task (Java 21)

Tip: For production, prefer constructing ThreadPoolExecutor directly with a bounded queue and an explicit rejection policy. newFixedThreadPool’s unbounded queue can mask overload and exhaust memory.

Callable and Future

Runnable returns nothing. Callable<T> returns a value and may throw checked exceptions. Submitting a Callable yields a Future<T> — a handle to a result that may not exist yet.

import java.util.concurrent.*;

ExecutorService pool = Executors.newSingleThreadExecutor();

Future<Integer> future = pool.submit(() -> {
    Thread.sleep(50);
    return 6 * 7;
});

Integer result = future.get(); // blocks until ready
System.out.println(result);
pool.shutdown();

Output:

42

Warning: future.get() blocks the calling thread indefinitely. Use the timed overload get(timeout, unit) to avoid hanging forever.

CompletableFuture

CompletableFuture enables non-blocking composition of asynchronous pipelines — chain transformations, combine results, and handle errors without ever calling get().

import java.util.concurrent.CompletableFuture;

CompletableFuture<String> pipeline =
    CompletableFuture.supplyAsync(() -> "alan")
        .thenApply(String::toUpperCase)        // transform
        .thenApply(s -> "Hello, " + s)         // chain
        .exceptionally(ex -> "fallback");      // recover

System.out.println(pipeline.join());

Output:

Hello, ALAN

Use thenCompose to flatten nested futures and thenCombine to merge two independent async results.

Concurrent Collections

The synchronized wrappers (Collections.synchronizedMap) lock the whole structure. Concurrent collections use finer-grained strategies for far better throughput under contention.

CollectionUse case
ConcurrentHashMapHigh-concurrency map; lock-striped, atomic compute/merge
CopyOnWriteArrayListMany reads, rare writes (e.g. listener lists)
ConcurrentLinkedQueueLock-free unbounded queue
BlockingQueue (LinkedBlockingQueue)Producer/consumer hand-off
import java.util.concurrent.ConcurrentHashMap;

ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
counts.merge("hits", 1, Integer::sum); // atomic increment, thread-safe

ReentrantLock vs synchronized

synchronized is simple and automatically released. ReentrantLock is an explicit lock offering features synchronized cannot: timed and interruptible acquisition, fairness, and tryLock.

import java.util.concurrent.locks.ReentrantLock;

ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    // critical section
} finally {
    lock.unlock(); // MUST be in finally
}
FeaturesynchronizedReentrantLock
Acquire/releaseAutomaticManual (lock/unlock)
Try with timeoutNotryLock(time, unit)
InterruptibleNolockInterruptibly()
Fairness optionNoYes
Condition objectsOne (wait/notify)Multiple (newCondition)

Warning: With ReentrantLock, always unlock() in a finally block. Forgetting to unlock leaks the lock and can deadlock the whole application.

Atomic Classes

For single-variable updates, atomic classes (AtomicInteger, AtomicLong, AtomicReference) provide lock-free thread safety via CPU compare-and-swap (CAS) — faster than locking under contention.

import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();          // atomic ++
counter.updateAndGet(v -> v * 2);   // atomic transform
System.out.println(counter.get());

Output:

2

Best Practices

  • Submit tasks to executors instead of creating raw threads; always shutdown().
  • Bound your thread pools and queues, with an explicit rejection policy.
  • Compose async work with CompletableFuture rather than blocking on Future.get().
  • Reach for atomic classes for single counters; locks only for multi-variable invariants.
  • Always unlock() in finally; acquire multiple locks in a consistent global order to avoid deadlock.
  • Prefer concurrent collections over synchronized wrappers for shared, contended data.

Interview Questions

Q: What is the difference between Runnable and Callable? Runnable.run() returns void and cannot throw checked exceptions. Callable.call() returns a value and may throw checked exceptions. Submitting a Callable returns a Future for the eventual result.

Q: When would you use ReentrantLock over synchronized? When you need timed or interruptible lock acquisition, tryLock, fairness, or multiple condition variables. For simple mutual exclusion, synchronized is preferable because it cannot leak an unreleased lock.

Q: Why are atomic classes faster than synchronization for counters? They use hardware compare-and-swap (CAS) instructions to update a value without acquiring a lock, avoiding blocking and context switches. Under moderate contention this is significantly faster than monitor-based locking.

Q: How does ConcurrentHashMap differ from Collections.synchronizedMap? synchronizedMap guards the entire map with a single lock, serializing all access. ConcurrentHashMap uses fine-grained, lock-free reads and per-bin locking on writes, allowing high concurrent throughput, plus atomic operations like compute and merge.

Last updated June 1, 2026
Was this helpful?