Java concurrency is the set of language features and JDK classes that let a program run several tasks at the same time and safely share data between them. It covers threads and virtual threads, executors and CompletableFuture, locks and atomic variables, concurrent collections, and the rules of the Java Memory Model.
We use concurrency to serve many requests at once, to call several remote services in parallel instead of one after another, and to run background jobs without blocking the user. Since Java 21, virtual threads make the simple one-thread-per-task style scale to thousands of concurrent tasks.
The following example runs two lookups at the same time on virtual threads and waits for both results.
Future<String> user;
Future<Integer> cartSize;
try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) {
user = pool.submit(() -> "lokesh"); // runs on its own virtual thread
cartSize = pool.submit(() -> 3); // runs at the same time
} // close() waits for both tasks
String name = user.resultNow(); // "lokesh"
int items = cartSize.resultNow(); // 3
Notice that the code never creates a Thread by hand and never shares a mutable variable between the tasks. We explain what thread safety means, follow the concurrency features from Java 1.0 to Java 25, and give a map of all our concurrency tutorials, grouped by the task each one solves.
1. Concurrency and Threads in Java
Concurrency means that several tasks make progress during the same period, whereas parallelism means that they run at the same instant on different CPU cores. A web server that handles 500 open requests on 8 cores is concurrent, and it runs at most 8 of them in parallel at any moment. The article on concurrency vs parallelism compares both in detail.

In Java, every concurrent task runs on a thread. A platform thread wraps an operating system thread, which is expensive, so apps traditionally reuse a small pool of them. A virtual thread is a lightweight thread managed by the JVM, and when it blocks on I/O or a lock, it releases its carrier platform thread for other work. All threads of a JVM share the same heap, so any object that two threads can reach needs protection.
The JDK offers several layers on top of threads. Most application code works with the upper layers and rarely creates a Thread object by hand.
- Thread and Runnable are the foundation, together with the Java Memory Model, which defines when a write in one thread becomes visible to another.
- ExecutorService, ForkJoinPool and virtual threads decide where and when tasks run.
- synchronized, ReentrantLock, Semaphore and CountDownLatch coordinate threads.
- Atomic variables, volatile, concurrent collections and immutable objects keep shared data consistent.
- CompletableFuture, structured concurrency and scoped values describe tasks and their results at a higher level.
2. What Is Thread Safety?
A class is thread-safe when it behaves correctly while several threads use it at the same time, no matter how the scheduler interleaves those threads, and without any extra synchronization in the calling code. “Correctly” means that the class keeps its own rules, such as “a counter never loses an increment” or “a balance never goes below zero”. The definition from Java Concurrency in Practice is still the most useful one, because it ties safety to the behavior callers expect.
Put differently, a thread-safe class is no more broken with many threads than with one. The class hides the synchronization it needs, so callers do not have to add their own.
2.1. A Race Condition in Practice
A race condition happens when the result depends on the timing of threads. The classic example is count++, which looks like one step but is three, namely read the value, add one and write it back. When two threads read the same old value, one increment is lost.
class VisitCounter {
private int count;
void increment() {
count++; // read, add, write: not atomic
}
int get() {
return count;
}
}
VisitCounter visits = new VisitCounter();
ExecutorService pool = Executors.newFixedThreadPool(2);
for (int t = 0; t < 2; t++) {
pool.submit(() -> {
for (int i = 0; i < 100_000; i++) {
visits.increment();
}
});
}
pool.close();
int counted = visits.get(); // less than 200000 in most runs
Say a news site counts article views this way. Under load, the counter reports fewer views than the access log, and the bug never shows up in a unit test with one thread. The safe version replaces the int with an AtomicInteger, whose incrementAndGet() uses a compare-and-swap (CAS) instruction instead of a lock.
AtomicInteger views = new AtomicInteger();
ExecutorService pool = Executors.newFixedThreadPool(2);
for (int t = 0; t < 2; t++) {
pool.submit(() -> {
for (int i = 0; i < 100_000; i++) {
views.incrementAndGet();
}
});
}
pool.close();
int total = views.get(); // 200000
2.2. Stateless Code Is Thread-Safe
An object without fields cannot be broken by other threads, because every call keeps its data in local variables on its own thread stack. A Spring controller or a servlet is a singleton that serves all requests, and it stays thread-safe as long as it keeps request data in locals and parameters instead of fields.
static List<Integer> primeFactors(int number) {
List<Integer> factors = new ArrayList<>(); // local, one list per call
for (int p = 2; number > 1; p++) {
while (number % p == 0) {
factors.add(p);
number /= p;
}
}
return factors;
}
List<Integer> factors = primeFactors(84); // [2, 2, 3, 7]
Two threads calling primeFactors() at the same time each get their own factors list, so they cannot affect each other. The article on local variables and thread safety explains where this rule stops, for example when a local variable points to a shared object.
2.3. Ways to Make Code Thread-Safe
The safest shared state is no shared state, so we try to avoid sharing first and add locks or atomics only where sharing is unavoidable. Each technique fits a different kind of state.
| Technique | How it works | Good for |
|---|---|---|
| Confinement | Data stays in locals or in one thread | Request handling, pure functions |
| Immutability | Objects never change after construction, for example records with final fields | Configuration, value objects, messages |
| volatile | Every read sees the latest write, but compound actions stay unsafe | Flags such as running, see volatile variables |
| Atomic variables | Lock-free single-variable updates with CAS | Counters, sequence numbers, references |
| synchronized and locks | One thread at a time in a critical section | Invariants across several fields |
| Concurrent collections | Thread-safe maps, lists and queues | Caches, registries, work queues |
| Per-thread data | Each thread or scope gets its own copy | Request context, non-thread-safe helpers such as old SimpleDateFormat |
3. Evolution of Java Concurrency up to Java 25
Java has had threads since version 1.0, but the tools changed a lot. Java 5 added the java.util.concurrent package, and Java 21 changed the cost of a thread with virtual threads. Knowing the version of a feature helps when we read old code or old tutorials.
| Java version | What it added for concurrency |
|---|---|
| 1.0 (1996) | Thread, Runnable, synchronized, volatile, wait()/notify(), Hashtable and Vector |
| 1.2 | ThreadLocal, synchronized collection wrappers such as Collections.synchronizedMap(), and the deprecation of Thread.stop(), suspend() and resume() |
| 5 (2004) | The java.util.concurrent package (JSR 166) with executors, Callable and Future, locks, atomics, ConcurrentHashMap, blocking queues, CountDownLatch, CyclicBarrier and Semaphore, plus the revised Java Memory Model (JSR 133) |
| 6 | ConcurrentSkipListMap, ConcurrentSkipListSet and LinkedBlockingDeque |
| 7 | Fork/join framework (ForkJoinPool), Phaser, LinkedTransferQueue, ConcurrentLinkedDeque and ThreadLocalRandom |
| 8 | CompletableFuture, StampedLock, LongAdder, parallel streams and a rewritten ConcurrentHashMap with bulk operations |
| 9 | Reactive streams interfaces in Flow, VarHandle, and timeouts for CompletableFuture |
| 19 | ExecutorService implements AutoCloseable, and Future gains state(), resultNow() and exceptionNow() |
| 20 | Thread.stop() throws UnsupportedOperationException |
| 21 | Virtual threads are final (JEP 444), with Thread.ofVirtual(), Thread.ofPlatform() and Executors.newVirtualThreadPerTaskExecutor() |
| 23 | Thread.suspend() and Thread.resume() are removed |
| 24 | Virtual threads no longer pin their carrier inside synchronized (JEP 491) |
| 25 | Scoped values are final (JEP 506), and structured concurrency is in its fifth preview with the new StructuredTaskScope.open() and Joiner API (JEP 505) |
Two practical rules follow from the timeline. Code that still calls Thread.stop() fails at runtime on current JDKs, so we stop threads by interrupting them or with a flag, as the posts in section 4.1 show. Structured concurrency needs –enable-preview on Java 25, so we keep it out of production code until it becomes final.
4. Java Concurrency Tutorials by Task
Our tutorials each cover one class or one problem in depth. The tables group them by what we want to get done, so we can go straight to the right one.
4.1. Creating and Controlling Threads
Our thread basics posts cover the Thread class itself. They are the base for everything else, even if we later hand threads over to an executor.
| Tutorial | What it covers |
|---|---|
| Create and start a thread | Thread, Runnable, lambdas, builders and ThreadFactory |
| Runnable vs Thread | Implementing Runnable or extending Thread |
| Runnable vs Callable | Tasks without and with a return value |
| Thread names | Naming threads for logs and dumps |
| Thread life cycle and states | NEW, RUNNABLE, BLOCKED, WAITING and the transitions |
| Daemon threads | Background threads that do not keep the JVM alive |
| Joining threads | join(), timeouts and Thread.yield() |
| Waiting for threads to finish | join(), CountDownLatch and executors |
| Stopping a thread | Flags and interruption instead of stop() |
| Handling InterruptedException | Restoring the interrupt flag and cancelling cleanly |
| Thread.sleep() vs wait() | Pausing a thread versus waiting on a monitor |
| UncaughtExceptionHandler | Logging and restarting threads that die |
4.2. Virtual Threads, Structured Concurrency and Scoped Values
Project Loom delivered the features in this group. Virtual threads and scoped values are final in Java 25, while structured concurrency is still a preview.
| Tutorial | What it covers |
|---|---|
| Virtual threads | How virtual threads work and when they help |
| newVirtualThreadPerTaskExecutor() | One virtual thread per task with an executor |
| Virtual threads in Spring Boot | Turning virtual threads on for web requests |
| Structured concurrency | StructuredTaskScope and joiners (preview) |
| Scoped values | Sharing request data without ThreadLocal |
4.3. Executors, Thread Pools and Async Tasks
An executor separates submitting a task from running it. These posts cover the pool types, the lifecycle and the result handling.
| Tutorial | What it covers |
|---|---|
| ExecutorService guide | Creating executors and submitting tasks |
| Thread pools and ThreadPoolExecutor | Core and maximum size, queues and fixed-size pools |
| ThreadPoolExecutor with BlockingQueue | Choosing the work queue of a pool |
| RejectedExecutionHandler | What happens when the pool and queue are full |
| ScheduledExecutorService | Delayed and periodic tasks |
| invokeAll() and invokeAny() | Running a batch of tasks and collecting the results |
| Callable and Future | Getting results and exceptions from tasks |
| Cancelling a task | Future.cancel() and interruption |
| Shutting down an executor | shutdown(), shutdownNow() and close() |
| CompletableFuture | Chaining, combining and timing out async tasks |
| Fork/join framework | Splitting CPU-bound work with ForkJoinPool |
4.4. Locks and Thread Coordination
The coordination posts show how threads take turns and wait for each other. Most coordination problems in real apps come down to one of these tools.
| Tutorial | What it covers |
|---|---|
| Object vs class level locking | What synchronized locks on |
| wait(), notify() and notifyAll() | Monitor-based signaling |
| IllegalMonitorStateException | Calling monitor methods without holding the lock |
| Lock and ReentrantLock | Explicit locks, tryLock() and conditions |
| Semaphore | Limiting concurrent access and throttling |
| Binary semaphore | A semaphore with one permit as a lock |
| Semaphore vs ReentrantLock | Choosing between permits and ownership |
| CountDownLatch | Waiting until a number of events happen |
| Deadlocks | Creating, detecting and avoiding a deadlock |
4.5. Shared Data and Memory Visibility
Per-thread copies of data are covered in the ThreadLocal tutorial, including the memory leak that a forgotten remove() causes in thread pools. The posts on local variables, volatile, AtomicInteger and compare-and-swap are linked from section 2, where we compare all the techniques side by side.
4.6. Concurrent Collections and Queues
A concurrent collection lets many threads read and write without an external lock. Producer-consumer designs and thread pools are built on blocking queues.
| Tutorial | What it covers |
|---|---|
| ConcurrentHashMap | Atomic updates, bulk operations and sizing |
| ConcurrentMap | The interface and its atomic methods |
| Synchronizing a HashMap | synchronizedMap() versus ConcurrentHashMap |
| ConcurrentSkipListMap | A sorted concurrent map |
| CopyOnWriteArrayList | Lists that are read far more often than changed |
| CopyOnWriteArraySet | The set version of copy-on-write |
| ConcurrentLinkedDeque | A non-blocking deque |
| ArrayBlockingQueue | A bounded blocking queue backed by an array |
| PriorityBlockingQueue | A blocking queue ordered by priority |
| DelayQueue | Elements that become available after a delay |
| SynchronousQueue | A hand-off queue with no capacity |
| LinkedTransferQueue | Producers that wait until a consumer receives |
| Producer-consumer with BlockingQueue | Bounded buffers, poison pills and backpressure |
4.7. Troubleshooting Concurrent Code
When a concurrent app hangs or burns CPU, a thread dump shows what every thread is doing.
| Tutorial | What it covers |
|---|---|
| Thread dumps | Capturing and reading dumps, including virtual threads |
| jstack on Linux | Taking a dump from the command line |
5. Choosing the Right Concurrency Tool
Most concurrency problems in business apps fall into a few shapes. Starting from the shape of the problem, rather than from the API we know best, leads to simpler code.
| Task | Use in Java 25 | Avoid |
|---|---|---|
| Run many blocking calls (HTTP, JDBC) at once | Virtual threads, newVirtualThreadPerTaskExecutor() | Huge platform thread pools |
| Run CPU-heavy work on all cores | A fixed pool sized to the cores, ForkJoinPool or parallel streams | Virtual threads, which add nothing for CPU work |
| Combine results of async calls | CompletableFuture, or structured concurrency once it is final | Threads that write results into shared fields |
| Count or sum from many threads | AtomicInteger, LongAdder | synchronized around a plain int on hot paths |
| Share a cache or registry | ConcurrentHashMap with computeIfAbsent() | HashMap with outside locking |
| Limit calls to a slow service | Semaphore | A small thread pool used only as a limiter |
| Hand work from producers to workers | A bounded BlockingQueue | wait()/notify() written by hand |
| Wait for N tasks to finish | ExecutorService.close(), invokeAll(), CountDownLatch | Polling a shared flag |
| Pass request context down the call stack | ScopedValue | ThreadLocal with virtual threads in large numbers |
Say a trip planner page needs a flight quote, a hotel quote and a weather forecast, each from a different service with about 100 ms latency. Called one after another, the page waits about 300 ms, while three tasks on virtual threads finish in about the time of the slowest call. The trip total itself is CPU work of a few microseconds, so it runs on the request thread and needs no executor at all.
6. Common Concurrency Bugs
Concurrency bugs are hard because they depend on timing. They rarely show up on a developer laptop and appear under production load, often only once a day. Knowing their names helps us recognize them in a thread dump or a bug report.
| Bug | What happens | Typical fix |
|---|---|---|
| Race condition | The result depends on thread timing, for example lost updates | Atomic variables, locks or confinement |
| Visibility problem | A thread never sees another thread’s write, for example a stop flag | volatile, locks or concurrent classes |
| Deadlock | Threads wait for each other’s locks forever | Take locks in one global order, use tryLock() with a timeout |
| Starvation | A thread never gets a lock or CPU time | Fair locks, shorter critical sections |
| Thread leak | Executors are never shut down, so threads pile up | try-with-resources on ExecutorService |
| Lost interrupt | Code swallows InterruptedException, so cancellation stops working | Restore the flag with Thread.currentThread().interrupt() |
7. Java Concurrency FAQs
Virtual threads changed parts of the classic advice, so these answers separate what still applies from what is outdated.
7.1. Do we still need to learn threads and synchronized when virtual threads exist?
Yes. Virtual threads are still threads, so race conditions, visibility rules, locks and the memory model apply to them the same way as before. What changed is the cost of a thread, which makes thread pools for blocking work unnecessary.
7.2. In which order should we learn Java concurrency?
Start with threads and thread safety, because every other tool builds on them. Continue with executors and Future, concurrent collections and atomic variables, and move on to CompletableFuture and virtual threads. Locks, semaphores and wait()/notify() come after that, since most application code needs them less often than the higher-level tools.
7.3. What is the difference between multithreading and concurrency?
Multithreading is one way to achieve concurrency, namely by running tasks on several threads. Concurrency is the broader idea of dealing with several tasks at once, which also includes event loops and asynchronous I/O on a single thread.
7.4. Is HashMap thread-safe?
No. Concurrent writes to a HashMap can lose entries or corrupt its internal structure. Use ConcurrentHashMap for shared maps, or wrap the map with Collections.synchronizedMap() when contention is low.
7.5. Is Java Concurrency in Practice still worth reading?
Yes, for the fundamentals. The book is from 2006, so it predates CompletableFuture, virtual threads and structured concurrency, but its chapters on thread safety, visibility and composing thread-safe classes are still accurate. We read it together with the JDK 25 Javadoc of java.util.concurrent for the newer APIs.
8. Conclusion
Java concurrency rests on threads that share one heap, and thread safety is about keeping a class correct while several of them use it. We avoid sharing where we can, prefer immutable objects and atomic variables, and use locks only when an invariant spans several fields.
The tools changed a lot since Java 5, and Java 21 to 25 changed them again with virtual threads, scoped values and structured concurrency in preview. For blocking I/O, virtual threads with an executor are the default today, for CPU work a sized pool is, and the tutorials in section 4 cover each tool in depth.
9. References
- java.util.concurrent package summary (Java 25)
- JLS 25, Chapter 17, Threads and Locks
- JEP 444, Virtual Threads
- JEP 505, Structured Concurrency (Fifth Preview)
- JEP 506, Scoped Values
- Oracle Java Tutorials, Concurrency
Happy Learning !!