Java, Concurrency
Java Concurrency and Multithreading Guide
· Eric B.
{/* Secondary: Thread, Runnable, ExecutorService, synchronized, ReentrantLock, volatile, Java Memory Model, BlockingQueue, ConcurrentHashMap, AtomicInteger, ThreadLocal, virtual threads. Context: JVM, jstack, VisualVM, JConsole, happens-before, race condition, deadlock. Values: 6 thread states, priority 1-10, Java 8 lambdas, Java 21 virtual threads, pricing $29/$49/$119. */}

Java multithreading runs several units of work inside one program so the CPU can make progress on more than one task at a time. A thread is the smallest schedulable unit of execution, and Java ships first-class support for threads through the Thread class, the Runnable interface, and the java.util.concurrent package. This guide walks every layer of that stack with runnable examples, from creating a single thread to tuning a thread pool, and it flags the bugs (race conditions, deadlocks, stale reads) that break concurrent code in production. If you are stuck on a graded assignment, GeeksProgramming has paired students with working Java developers since 2014, with a 50% deposit and the other 50% due only after the code runs.
What concurrency and parallelism mean in Java
Concurrency and parallelism describe two different things, and mixing them up causes most early confusion. Concurrency is structuring a program as independent tasks that can run out of order or interleaved. Parallelism is those tasks executing at the same instant on separate cores. A single-core machine runs Java threads concurrently but not in parallel; the scheduler slices CPU time between them fast enough to look simultaneous.
Java gives you concurrency through threads regardless of hardware. Whether that concurrency becomes parallelism depends on the core count and the JVM scheduler. The distinction matters because parallel speedup is bounded by available cores and by the share of work that must run sequentially, while a poorly synchronized concurrent program produces wrong answers on any number of cores.
| Term | What it means | Depends on | | --- | --- | --- | | Concurrency | Tasks structured to make progress independently | Program design | | Parallelism | Tasks executing at the same instant | CPU core count | | Thread | Smallest unit the scheduler runs | The JVM and OS |
How Java represents a thread
A Java thread is an OS-backed unit of execution wrapped by the java.lang.Thread class, with its own call stack and program counter but shared heap memory. Every program starts with one thread, the main thread, and the JVM also runs daemon threads such as the garbage collector in the background. Threads share the heap, which is exactly why shared mutable state is the source of concurrency bugs: two threads reading and writing the same object need coordination.
A thread moves through 6 states during its life, defined by the Thread.State enum. Reading a thread dump or debugging a hang starts with identifying which state each thread sits in.
- New: the
Threadobject exists butstart()has not been called. - Runnable: the thread is eligible to run and either executing or waiting for a CPU slot.
- Blocked: the thread is waiting to acquire a monitor lock held by another thread.
- Waiting: the thread waits indefinitely for another thread, after calling
wait(),join(), orLockSupport.park(). - Timed waiting: the same as waiting but with a deadline, after
sleep(ms)orwait(ms). - Terminated:
run()has returned or thrown, and the thread will not run again.
Java also assigns each thread a priority from 1 to 10, which the JVM passes to the OS scheduler as a hint. Priorities bias scheduling but guarantee nothing across platforms, so correct code never depends on one thread outrunning another.

How to create threads in Java
Java offers three ways to define the work a thread runs: extend Thread, implement Runnable, or pass a lambda. The recommended choice is Runnable or a lambda, because both separate the task from the thread that executes it and leave your class free to inherit something else.
Extending the Thread class
You can subclass Thread and override run(). It reads cleanly for a one-off, but it spends your single inheritance slot and ties the task to a specific thread.
class PrintTask extends Thread {
@Override
public void run() {
System.out.println("Running on " + Thread.currentThread().getName());
}
}
PrintTask task = new PrintTask();
task.start(); // start() spawns a new thread; calling run() directly does not
Call start(), never run(). Calling run() directly executes the body on the current thread and spawns nothing.
Implementing the Runnable interface
Runnable holds only the task. You hand the task to a Thread, or later to an ExecutorService, without changing the task itself. This is the preferred pattern.
class PrintTask implements Runnable {
@Override
public void run() {
System.out.println("Running on " + Thread.currentThread().getName());
}
}
Thread thread = new Thread(new PrintTask());
thread.start();
Lambda expressions for short tasks
Java 8 made Runnable a functional interface, so a lambda replaces the class entirely for short bodies.
Thread thread = new Thread(() -> {
System.out.println("Running on " + Thread.currentThread().getName());
});
thread.start();
The table below compares the three approaches so you can pick by intent rather than habit.
| Approach | Best for | Drawback |
| --- | --- | --- |
| Extend Thread | A quick standalone thread | Uses the single inheritance slot |
| Implement Runnable | Reusable tasks, pool submission | Slightly more code than a lambda |
| Lambda | Short inline tasks | Hard to reuse or name |
Thread synchronization and shared state
Synchronization coordinates threads so that only one touches a shared resource at a time, which protects data from corruption when several threads read and write it. Without it, two threads can interleave their operations and leave an object in a state neither one intended. That is a race condition, and it is the most common multithreading defect.
The synchronized keyword
The synchronized keyword locks on an object's monitor. A synchronized method holds the lock on this for its whole body, so two threads cannot run synchronized methods on the same instance at once.
public class Counter {
private int count = 0;
public synchronized void increment() {
count++; // read, add, write: atomic only because the method is synchronized
}
public synchronized int get() {
return count;
}
}
A synchronized block locks a smaller region on a chosen object, which shortens the time the lock is held and improves throughput.
public class Tally {
private final Object lock = new Object();
private int count = 0;
public void increment() {
synchronized (lock) {
count++;
}
}
}
Explicit locks with ReentrantLock
The java.util.concurrent.locks package gives you ReentrantLock when you need more than synchronized offers: a fair ordering policy, an interruptible acquire, or tryLock with a timeout. The cost is that you release the lock yourself, always in a finally block.
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private final ReentrantLock lock = new ReentrantLock();
private long balance = 0;
public void deposit(long amount) {
lock.lock();
try {
balance += amount;
} finally {
lock.unlock(); // releasing in finally prevents a permanent lock on exception
}
}
}
Atomic variables for lock-free counters
For a single number updated by many threads, AtomicInteger and AtomicLong skip locks entirely. They use a compare-and-swap CPU instruction, so incrementAndGet() is atomic without a monitor.
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger hits = new AtomicInteger(0);
hits.incrementAndGet(); // atomic, no lock held
Thread communication: producer and consumer
Threads often need to hand work to each other, and the producer-consumer pattern is the classic case: one thread generates items, another processes them, and they coordinate so the producer waits when the buffer is full and the consumer waits when it is empty. Java offers a low-level path with wait() and notify() and a high-level path with BlockingQueue.
The low-level wait and notify approach
wait() releases the monitor and parks the thread until another thread calls notify() on the same object. The call always sits inside a while loop, never an if, because a thread can wake without the condition being true (a spurious wakeup).
public class Buffer {
private int data;
private boolean available = false;
public synchronized void produce(int value) throws InterruptedException {
while (available) {
wait(); // loop, not if, to guard against spurious wakeups
}
data = value;
available = true;
notifyAll();
}
public synchronized int consume() throws InterruptedException {
while (!available) {
wait();
}
available = false;
notifyAll();
return data;
}
}
The BlockingQueue approach
BlockingQueue from java.util.concurrent does the same coordination with none of the manual locking. put() blocks when the queue is full, take() blocks when it is empty, and the class handles the waiting and signaling internally. Reach for this first.
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(100);
// Producer thread
queue.put(42); // blocks if the queue is full
// Consumer thread
int value = queue.take(); // blocks if the queue is empty
The wait/notify version teaches the mechanism, but BlockingQueue is what production code uses. Fewer lines, no missed-signal bugs.
Thread pools and ExecutorService
A thread pool keeps a set of worker threads alive and feeds them tasks from a queue, instead of creating a fresh thread for every job. Thread creation costs memory and CPU, and an unbounded thread-per-task design can exhaust the OS under load. A pool caps the number of live threads, removes creation overhead from the hot path, and gives you one place to tune capacity.
ExecutorService is the standard interface for submitting work to a pool. The Executors factory builds common pool shapes.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> {
System.out.println("Task on " + Thread.currentThread().getName());
});
pool.shutdown(); // stops accepting tasks and lets running ones finish
For control over the queue, the rejection policy, and the keep-alive time, build a ThreadPoolExecutor directly rather than through the factory.
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // core pool size
8, // maximum pool size
60L, TimeUnit.SECONDS, // idle keep-alive before extra threads die
new LinkedBlockingQueue<>(1000) // bounded task queue
);
A bounded queue matters. An unbounded queue hides a slow consumer until the program runs out of heap, so size the queue and choose a rejection policy on purpose. A common starting point for CPU-bound work is a pool sized to Runtime.getRuntime().availableProcessors().
Thread coordination and control
Java gives you direct controls to sequence and pause threads, the main ones being join(), sleep(), yield(), and the interruption methods. Each one answers a specific coordination question rather than serving as a general-purpose pause.
join()makes the calling thread wait until the target thread finishes. Use it to collect results before continuing.sleep(ms)suspends the current thread for a set time without releasing any lock it holds. Useful for retries with backoff, risky inside a synchronized block.yield()hints to the scheduler that the current thread will give up its slot. It is only a hint, so do not rely on it for correctness.interrupt()sets a thread's interrupt flag to request a cooperative stop.
Interruption is how you cancel a thread in Java, because Thread.stop() is unsafe and removed. A thread checks Thread.currentThread().isInterrupted() (or catches InterruptedException from a blocking call) and shuts down its own work in response.
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// do a unit of work, then loop
}
System.out.println("Interrupted, cleaning up");
});
worker.start();
worker.interrupt(); // requests a stop; the worker decides how to honor it
When a blocking call throws InterruptedException, restore the flag with Thread.currentThread().interrupt() before returning, so callers up the stack also see the cancellation.
ThreadLocal for per-thread state
ThreadLocal gives each thread its own private copy of a variable, which removes the need to synchronize state that is never actually shared. A common use is holding a SimpleDateFormat or a database connection per thread, since those objects are not thread-safe to share.
ThreadLocal<StringBuilder> buffer =
ThreadLocal.withInitial(StringBuilder::new);
buffer.get().append("thread-private data"); // each thread sees only its own builder
Always call remove() when a pooled thread finishes a task. Pool threads outlive tasks, so a forgotten ThreadLocal value leaks into the next task and into memory.
Thread safety strategies and common bugs
Thread safety means a class behaves correctly when several threads use it at once, with no data corruption and no result that depends on timing. Three strategies get you there, and four bug classes are what go wrong when they are missing.
Strategies for thread safety:
- Synchronization guards shared mutable state with
synchronized, aLock, or atomic types so only one thread mutates at a time. - Immutability removes the problem at the root: an object whose fields never change after construction needs no locking. Favor
finalfields and immutable value types. - Confinement keeps state inside one thread, through
ThreadLocalor by never publishing a reference, so no sharing exists to coordinate.
Common concurrency bugs:
- Race condition: two threads interleave a read-modify-write and one update is lost. Fix with synchronization or an atomic type.
- Deadlock: thread A holds lock 1 and waits for lock 2 while thread B holds lock 2 and waits for lock 1. Fix by acquiring locks in one global order and using
tryLockwith a timeout. - Stale read: a thread reads a cached value and misses another thread's write. Fix with
volatileor proper synchronization, both of which establish visibility. - Livelock: threads keep responding to each other and make no progress. Fix by adding randomized backoff so they stop reacting in lockstep.
For input validation and resource cleanup inside these methods, the patterns in our Exception Handling in Java: Full Guide carry directly into the try/finally blocks that release locks safely.
The Java Memory Model and the volatile keyword
The Java Memory Model (JMM) defines when a write by one thread becomes visible to a read by another, which is the rule set that makes concurrent code predictable across CPUs and compilers. Without the JMM, a compiler reordering or a per-core cache could let one thread see a value another thread already changed, or never see it at all. The central concept is happens-before: if action A happens-before action B, then B sees A's effects.
The volatile keyword is the lightweight tool the JMM provides for visibility. A volatile read goes to main memory and a volatile write flushes to main memory, so a value written by one thread is immediately visible to every other thread.
public class Worker {
private volatile boolean running = true; // visible to all threads on every read
public void stop() {
running = false;
}
public void run() {
while (running) {
// without volatile, this loop can read a cached true forever
}
}
}
Three misconceptions about volatile cause real bugs:
volatiledoes not make compound operations atomic.count++on avolatile intstill loses updates under contention, because it is three steps (read, add, write). UseAtomicIntegeror a lock.volatileis not a replacement for synchronization. It provides visibility, not mutual exclusion. A critical section that spans multiple fields needs a lock.volatileis not slow enough to avoid. Avolatileflag is cheap and is the correct tool for a single read-by-many, write-by-one signal.
Concurrent collections
Concurrent collections are thread-safe data structures in java.util.concurrent that let many threads read and write at once without external locking. They replace the legacy approach of wrapping a HashMap in Collections.synchronizedMap, which locks the whole map per operation and serializes every access.
| Collection | Use it for | Replaces |
| --- | --- | --- |
| ConcurrentHashMap | High-throughput shared key-value access | Hashtable, synchronized HashMap |
| CopyOnWriteArrayList | Read-heavy lists with rare writes | synchronized ArrayList |
| ConcurrentLinkedQueue | Lock-free unbounded queue | synchronized LinkedList |
| BlockingQueue | Producer-consumer handoff with backpressure | manual wait/notify |
ConcurrentHashMap deserves a note on compound operations. Each single call is thread-safe, but a check-then-act sequence of two calls is not atomic. Use the atomic methods built for it.
import java.util.concurrent.ConcurrentHashMap;
ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
// Not atomic: another thread can write between the get and the put
// if (!counts.containsKey(key)) counts.put(key, 0);
// Atomic: one operation
counts.merge(key, 1, Integer::sum); // increments, or inserts 1 if absent
If you want a broader tour of when to pick a map, set, queue, or list, our Java Collections Framework Explained guide covers the non-concurrent base classes these thread-safe variants extend.
Virtual threads in modern Java
Virtual threads, finalized in Java 21, are lightweight threads scheduled by the JVM instead of the operating system, so a program can run millions of them for blocking I/O work. A traditional platform thread maps one-to-one to an OS thread and costs around a megabyte of stack, which caps practical counts in the low thousands. A virtual thread parks cheaply when it blocks and hands its carrier thread to another task, which makes the thread-per-request model viable again.
// Java 21+: one virtual thread per task, no pool needed for I/O work
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
// a blocking call here parks the virtual thread, not an OS thread
return fetchFromDatabase();
});
}
Virtual threads cut the need to pool threads for I/O-bound code, since blocking one is cheap. CPU-bound work is different: it is limited by core count, so bounded pools sized to availableProcessors() and the fork/join framework still win there. Pick the model by workload, not by version number.
Debugging and profiling multithreaded code
Concurrency bugs hide because they depend on timing, so the right tools beat re-running and hoping. Four tools cover most investigations.
jstackprints a thread dump: every thread, its state, its stack, and the locks it holds or waits on. A deadlock shows up as two threads each waiting on a lock the other holds.- VisualVM is a free GUI for live monitoring of thread states, CPU, and memory, with a built-in thread dump viewer.
- JConsole ships with the JDK and detects deadlocks on demand through its Threads tab.
- Java Flight Recorder records low-overhead runtime events for after-the-fact analysis of contention and lock waits.
Add structured logging that includes Thread.currentThread().getName() on every line, so the interleaving is readable in the log. Reproduce the bug under load with a stress test before you claim a fix, because a concurrency bug that appears once in a thousand runs is not fixed by passing once.
Real-world multithreading examples
Multithreading earns its complexity in I/O-bound servers, CPU-bound batch jobs, and responsive user interfaces. Three short examples show the shape of each.
Web server handling many requests
A server submits each connection to a thread pool so one slow client never blocks the others.
ExecutorService pool = Executors.newFixedThreadPool(50);
while (true) {
Socket client = serverSocket.accept();
pool.submit(() -> handleRequest(client));
}
Parallel data processing
A parallel stream splits a large computation across cores using the common fork/join pool. This pays off for CPU-bound work on large datasets, not for short or I/O-bound tasks.
import java.util.stream.IntStream;
long primeCount = IntStream.rangeClosed(2, 1_000_000)
.parallel()
.filter(Worker::isPrime)
.count();
Keeping a UI responsive
A desktop UI runs long work off the UI thread, then posts the result back, so the interface never freezes. Swing uses SwingWorker for this split.
SwingWorker<String, Void> task = new SwingWorker<>() {
@Override
protected String doInBackground() {
return computeResult(); // runs off the Event Dispatch Thread
}
@Override
protected void done() {
try {
label.setText(get()); // back on the EDT, safe to touch the UI
} catch (Exception ex) {
label.setText("Failed: " + ex.getMessage());
}
}
};
task.execute();
These patterns also pair with disk work; combining a thread pool with the streams covered in Java File I/O: Read, Write, and Manage Files is how batch jobs read and write files in parallel without blocking the main thread.
Best practices for writing concurrent Java
Correct concurrent code follows a short set of habits that prevent the bugs above before they happen. Eight rules cover the majority of assignment and production code.
- Prefer
java.util.concurrentover hand-rolled locks.ExecutorService,BlockingQueue, and the atomic types are tested and faster than custom synchronization. - Make state immutable when you can. An immutable object is thread-safe with no locking.
- Hold locks for the shortest time possible. Do slow work outside the critical section.
- Acquire multiple locks in one global order to rule out deadlock.
- Loop on the condition around
wait(), never anif, to survive spurious wakeups. - Bound every queue and pool so backpressure surfaces instead of an out-of-memory error.
- Honor interruption by checking the flag and restoring it after catching
InterruptedException. - Test under load, since a concurrency bug that shows once in a thousand runs is still a bug.
These rules turn multithreading from a source of intermittent failures into a tool you can reason about. For a course project graded on correctness and clean structure, Java assignment help connects you with developers who write to these conventions and explain each choice. Pricing runs $29, $49, and $119 by tier, you pay half up front and half after the code runs, and an NDA keeps the work private.
Frequently asked questions
What is the difference between concurrency and parallelism in Java?
Concurrency means a program makes progress on more than one task over the same period, even on a single core, by interleaving execution. Parallelism means tasks run at the same instant on separate cores. Java threads give you concurrency; whether they run in parallel depends on the number of cores and how the JVM schedules them.
Should I extend Thread or implement Runnable?
Implement Runnable (or use a lambda) in almost every case. Extending Thread locks you out of inheriting any other class, since Java has single inheritance, and it couples your task to the thread that runs it. Runnable separates the work from the worker, so the same task can run on a raw Thread, an ExecutorService, or a thread pool.
What does the synchronized keyword actually guarantee?
It guarantees mutual exclusion and visibility. Only one thread holds a given monitor lock at a time, so synchronized blocks on the same lock never run concurrently. It also establishes a happens-before relationship: changes one thread makes before releasing the lock are visible to the next thread that acquires it.
When should I use volatile instead of synchronized?
Use volatile for a single variable that one thread writes and others read, such as a stop flag, where you need visibility but not atomic compound updates. Use synchronized or a Lock when a thread performs read-modify-write operations like incrementing a counter, because volatile does not make i++ atomic.
How do I avoid a deadlock in Java?
Acquire locks in the same global order across every thread, hold each lock for the shortest time possible, and prefer tryLock with a timeout over the blocking lock when more than one lock is involved. A thread dump from jstack names the threads in a deadlock and the locks each one waits on, which makes the cycle easy to spot.
Why should I use a thread pool instead of creating threads directly?
Creating a new thread per task costs memory and CPU and can exhaust the OS under load. A thread pool reuses a fixed set of workers and queues incoming tasks, which caps resource use, removes thread creation overhead from the hot path, and gives you one place to tune capacity. ExecutorService is the standard entry point.
What are virtual threads and do they replace thread pools?
Virtual threads, finalized in Java 21, are lightweight threads scheduled by the JVM rather than the OS, so you can run millions of them for blocking I/O work. They reduce the need to pool threads for I/O-bound code, but CPU-bound work and the fork/join framework still benefit from bounded pools sized to the core count.
Is ConcurrentHashMap fully thread-safe for compound operations?
Each individual ConcurrentHashMap operation is thread-safe, but a check-then-act sequence of two calls is not atomic. Use the atomic methods the class provides, such as putIfAbsent, computeIfAbsent, compute, and merge, when an update depends on the current value.
Related articles
- Java
Java Swing Tutorial for Beginners
Learn Java Swing from scratch: build your first window, wire button events, master five layout managers, and assemble a working calculator GUI.
May 24, 2024
- Java
Java Collections Framework Explained
A practical Java Collections guide covering List, Set, Queue, Map, Comparable, the Collections utility class, time complexity, and Stream API with runnable code.
Sep 4, 2023
- Java
Advanced Java Data Management Techniques
Master advanced Java data management: optimize data structures, handle concurrent access, tune memory, and use serialization and compression in real applications.
May 3, 2024


