Concurrency and Multithreading
Understand threads, executors, synchronization, locks, atomics, visibility, happens-before, concurrent collections, futures, and virtual threads.
Threads and Runnable Tasks
A Java thread represents an execution path. Production applications generally benefit from executors that manage task scheduling and lifecycle.
Runnable task = () ->
System.out.println(Thread.currentThread().getName());
Thread thread = new Thread(task);
thread.start();
Calling start() creates a new execution path; calling run() directly is an ordinary method call on the current thread.
Shared mutable state is where concurrency becomes difficult. Ask who owns state, who can mutate it, and what synchronization establishes visibility and ordering.
start() schedules a new thread. run() is just an ordinary method call when invoked directly.
- Prefer task-based concurrency over many manually managed threads.
- Thread safety is primarily about shared mutable state.
- Know the difference between starting a thread and invoking run directly.
synchronized, Locks, and Visibility
synchronized provides mutual exclusion and memory-visibility guarantees. ReentrantLock offers additional features such as timed acquisition.
private final Object lock = new Object();
private int count;
void increment() {
synchronized (lock) {
count++;
}
}
int getCount() {
synchronized (lock) {
return count;
}
}
A data race occurs when concurrent access includes a write without appropriate synchronization. The Java Memory Model defines happens-before relationships.
volatile provides visibility and ordering guarantees but does not make compound operations such as count++ atomic.
synchronizedprovides mutual exclusion and visibility.volatiledoes not make compound read-modify-write operations atomic.- The Java Memory Model defines visibility guarantees.
Executors and CompletableFuture
Executors separate task submission from thread management. CompletableFuture composes asynchronous stages.
ExecutorService pool = Executors.newFixedThreadPool(4);
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> loadUser(), pool)
.thenApply(User::name);
future.thenAccept(System.out::println);
Choose executor sizes based on workload. CPU-bound and blocking I/O workloads usually need different strategies. Always define shutdown behavior for owned executors.
- Executors control concurrency resources.
CompletableFutureenables composition of asynchronous operations.- Thread-pool sizing depends on workload characteristics.
Concurrent Collections and Atomics
Java provides ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue, and atomic classes such as AtomicInteger and AtomicReference.
ConcurrentHashMap<String, LongAdder> counters =
new ConcurrentHashMap<>();
counters.computeIfAbsent("java", key -> new LongAdder())
.increment();
Concurrent collections provide well-defined concurrent operations. LongAdder can outperform AtomicLong under heavy contention because updates are distributed across cells.
- Use concurrent collections rather than manually synchronizing ordinary collections when appropriate.
- Atomics support thread-safe single-variable state transitions.
- Contention characteristics should guide counter selection.
Virtual Threads and Modern Concurrency
Virtual threads are lightweight Java threads designed to make high-concurrency blocking workloads easier to express.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<String> future =
executor.submit(() -> fetchRemoteData());
System.out.println(future.get());
}
Virtual threads do not make CPU-bound work faster. They improve scalability for workloads where many tasks spend time waiting. Concurrency still needs limits around databases, APIs, queues, and other external resources.
Use virtual threads to scale blocking task concurrency, not as a replacement for CPU parallelism or as permission to remove backpressure.
- Virtual threads are lightweight and suited to many blocking tasks.
- They do not increase CPU capacity.
- Concurrency still needs limits around external resources.