Java Concurrency Explained

Intermediate
9 min read· Backend & Databases

Concurrency lets a program make progress on multiple tasks at once. In Java, that means threads sharing memory — which is powerful but dangerous: two threads touching the same data without coordination cause race conditions. Java gives you a toolbox to coordinate safely: synchronized and locks for mutual exclusion, volatile for visibility, atomic classes for lock-free counters, and high-level executors and CompletableFuture so you rarely manage raw threads yourself.

Think of threads as cooks sharing one kitchen

Several cooks (threads) work in one kitchen (shared memory). If two grab the same knife (a shared variable) at once, chaos follows — a race condition. A lock is like a rule that only one cook may use the cutting board at a time. volatile is like a shared whiteboard everyone must read fresh instead of trusting their own memory. Executors are the head chef who hands tasks to available cooks so you never micromanage who does what.

Step by Step

1 / 5

Key Concepts

Java Memory Model (JMM)

The rules defining when a write by one thread becomes visible to another. It permits reordering for speed but guarantees visibility across synchronized, volatile, and java.util.concurrent primitives via "happens-before" relationships.

Deadlock

Two threads each hold a lock the other needs and wait forever. Avoid it by always acquiring multiple locks in the same global order, or using tryLock with timeouts.

ExecutorService

A managed pool of worker threads. You submit Runnable/Callable tasks and it schedules them, reusing threads instead of creating one per task. Always shut it down to avoid leaks.

CompletableFuture

A composable async result. It lets you chain transformations (thenApply), sequence dependent calls (thenCompose), run work in parallel, and combine results (allOf) without blocking threads.

Key Facts

  • synchronized gives you both mutual exclusion and visibility; volatile gives only visibility. Use volatile for simple flags, locks for compound updates.
  • Prefer the high-level java.util.concurrent classes (ConcurrentHashMap, BlockingQueue, AtomicLong) over hand-written locking — they are correct and highly optimised.
  • With Java 21 virtual threads, the classic thread-per-request model scales to millions of concurrent tasks with plain blocking code.

Real-World Applications

A thread-safe counter or cache

For a shared hit counter use AtomicLong; for a shared cache use ConcurrentHashMap. Both scale far better under contention than wrapping a plain HashMap in synchronized.

Parallel downstream calls

When a request needs data from three services, fire three CompletableFutures on an executor and combine with allOf. Total latency becomes the slowest call, not the sum of all three.

Frequently Asked Questions

What is the difference between synchronized and volatile?

synchronized enforces mutual exclusion (one thread at a time) and visibility, so it protects compound operations like count++. volatile only guarantees that reads/writes of that single variable are visible across threads — it does not make count++ atomic.

When should I use ReentrantLock instead of synchronized?

Use ReentrantLock when you need features synchronized lacks: a timed or interruptible lock attempt (tryLock), fairness, or separate read/write locks (ReentrantReadWriteLock). Otherwise synchronized is simpler and now well optimised.

How do I avoid race conditions?

Minimise shared mutable state; make data immutable where possible. When you must share, guard every read and write of that state with the same lock, or use thread-safe concurrent classes and atomics designed for the job.

Is ConcurrentHashMap fully thread-safe?

Each individual operation (get, put, computeIfAbsent) is atomic and thread-safe. But a sequence of operations you perform yourself is not automatically atomic — use the atomic methods it provides (like merge or compute) for read-modify-write patterns.

Related Topics