Concurrency means a program deals with several tasks in overlapping time periods, so their execution interleaves, whereas parallelism means the program runs several computations at the same instant on different CPU cores. Concurrency is about structuring many tasks, and parallelism is about finishing one job faster with more hardware.
We need concurrency in servers and UIs that handle many independent requests or events, most of which wait for I/O. We need parallelism for CPU-heavy jobs such as image processing, report aggregation or sorting a large array, which we split into parts that run on all cores.
The following example shows both in Java 25. The first part overlaps three waiting calls on virtual threads, and the second part spreads one CPU-bound count over all cores with a parallel stream.
// Concurrency: three calls that mostly wait, overlapped
List<Callable<String>> calls = List.of(
() -> callService("payment"), () -> callService("stock"), () -> callService("shipping"));
List<Future<String>> replies;
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
replies = executor.invokeAll(calls);
}
String firstReply = replies.get(0).resultNow(); // "payment ok"
// Parallelism: one CPU-bound job split across cores
long primes = LongStream.rangeClosed(2, 200_000).parallel().filter(n -> isPrime(n)).count(); // 17984
Notice that the three calls finish together because they wait at the same time, while the prime count finishes sooner because several cores compute at once. We define callService() and isPrime() in the first two sections, compare both ideas in tables, go through the four combinations, map each case to a Java API, and look at the cases where parallelism does not pay off.
1. What Is Concurrency?
A concurrent program has two or more tasks in progress during the same period. On a single core, the operating system runs one thread for a short time slice, switches to another, and returns later, so the tasks make progress in turns. A task that waits for a network reply gives up the CPU, and another task uses it in the meantime.

We measure concurrency by throughput, which is the number of tasks finished per second. Each task does not get faster, but the program keeps working while tasks wait, so it completes more of them in the same time.
Say a checkout service calls a payment service, a stock service and a shipping service for every order, and each call waits about 100 ms for the reply. Run one after another, an order takes 300 ms. Run concurrently, an order takes about 100 ms, even on a machine with one core, because the CPU is idle during the waits. The following callService() method stands in for such a call.
static String callService(String name) {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100)); // waiting for the reply
return name + " ok";
}
List<Callable<String>> orderCalls = List.of(
() -> callService("payment"), () -> callService("stock"), () -> callService("shipping"));
long start = System.nanoTime();
List<String> results = new ArrayList<>();
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (Future<String> reply : executor.invokeAll(orderCalls)) {
results.add(reply.resultNow());
}
}
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
String allReplies = String.join(", ", results); // "payment ok, stock ok, shipping ok"
boolean overlapped = elapsedMs < 250; // true, not 300 ms in sequence
In Java, the main tools for concurrency are threads, the ExecutorService and, since Java 21, virtual threads, which make one thread per waiting task affordable.
2. What Is Parallelism?
A parallel program splits one job into subtasks that run at the same instant on different cores, and combines their partial results at the end. Parallelism needs hardware with more than one processing unit, so on a single-core machine there is no parallelism, only concurrency.
We measure parallelism by latency, which is the time one job takes from start to end. With four cores and a job that splits well, the job can finish in close to a quarter of the time, minus the cost of splitting and combining.
Say a photo app must create thumbnails for 2,000 uploaded images. Each image is pure computation, with no waiting, so one thread uses one core and leaves the others idle. Splitting the list across all cores is what makes the batch faster. The following isPrime() method plays the role of such CPU-bound work.
static boolean isPrime(long n) {
if (n < 2) {
return false;
}
for (long d = 2; d * d <= n; d++) {
if (n % d == 0) {
return false;
}
}
return true;
}
int cores = Runtime.getRuntime().availableProcessors(); // depends on the machine, e.g. 8
long sequential = LongStream.rangeClosed(2, 200_000).filter(n -> isPrime(n)).count(); // 17984
long parallel = LongStream.rangeClosed(2, 200_000).parallel().filter(n -> isPrime(n)).count(); // 17984
boolean sameResult = sequential == parallel; // true
A parallel stream runs its parts in the common ForkJoinPool, whose default parallelism is the number of cores minus one, and the calling thread helps as well. For divide-and-conquer algorithms that do not fit a stream, we write a RecursiveTask for the Fork/Join framework.
3. Concurrency vs Parallelism Differences
A program can be concurrent without being parallel, and parallel without being concurrent, because the two terms describe different things. Concurrency describes how the program is structured, and parallelism describes how the hardware runs it.
| Aspect | Concurrency | Parallelism |
|---|---|---|
| Goal | Handle many tasks during the same period | Finish one job faster |
| Tasks | Many, mostly independent | One job split into related subtasks |
| Hardware | Works on one core | Needs several cores |
| Main metric | Throughput (tasks per second) | Latency (time per job) |
| Typical workload | I/O-bound, waiting on network, disk or users | CPU-bound computation |
| Number of threads | Can be far more than cores | About one per core |
| Java tools | Threads, ExecutorService, virtual threads, CompletableFuture | Parallel streams, ForkJoinPool, a fixed pool sized to cores |
The number of threads shows the difference well. A concurrent server gains from 10,000 virtual threads because they spend most of their time waiting, while a parallel computation gains nothing from more threads than cores, because each extra thread only waits for a free core.
4. The Four Combinations
Since the terms are independent, an app falls into one of four combinations.
| Combination | What happens | Java example |
|---|---|---|
| Neither concurrent nor parallel | One task runs from start to end on one thread | A command-line tool that reads a file and prints a total |
| Concurrent, not parallel | Several tasks interleave, but only one runs at any instant | Virtual threads on a JVM limited to one carrier, or a server on a single-core container |
| Parallel, not concurrent | One task at a time, split across cores | A batch job that runs one parallel stream after another |
| Concurrent and parallel | Several tasks interleave, and several run at the same instant | A web server on a multi-core machine handling many requests |
Most production Java apps are in the last row. The distinction still matters, because it tells us which problem we are solving and which tool to pick.
5. Choosing the Right Java API
The deciding question is whether the workload needs more threads or more cores. Waiting tasks need more threads, and computing tasks need more cores.
| Workload | Kind | Java API |
|---|---|---|
| Many requests that call databases and REST APIs | Concurrency | Virtual threads, newVirtualThreadPerTaskExecutor() |
| Several independent calls for one request | Concurrency | CompletableFuture or structured concurrency |
| Large in-memory collection, CPU-heavy per element | Parallelism | parallel() on a stream |
| Recursive split of a big array or tree | Parallelism | ForkJoinPool with RecursiveTask |
| Long-running CPU jobs submitted over time | Parallelism | Executors.newFixedThreadPool(cores) |
6. When Parallelism Does Not Pay Off
Parallel code has a cost for splitting the work, scheduling the parts and merging the results. That cost is only worth paying for enough CPU work, and it adds a few traps.
- Small inputs, such as a list of 100 elements with a cheap operation, run slower in parallel, because splitting costs more than the work.
- Blocking I/O inside a parallel stream ties up the few common pool threads, so other parallel streams in the app wait too.
- Operations such as forEach() lose the encounter order, so we use forEachOrdered() or a collector when the order matters.
- Shared mutable state gives wrong results, because several cores update it at the same time.
The last trap is the most common one. When many cores increment one array element without synchronization, some increments get lost.
int[] counter = {0};
IntStream.range(0, 100_000).parallel().forEach(i -> counter[0]++);
int unsafeTotal = counter[0]; // less than 100000 in most runs
The safe version lets the stream combine the partial results itself, so no thread writes to a shared variable.
int safeTotal = IntStream.range(0, 100_000).parallel().map(i -> 1).sum(); // 100000
List<Integer> evens = IntStream.range(0, 10).parallel()
.filter(i -> i % 2 == 0).boxed().toList();
List<Integer> ordered = evens; // [0, 2, 4, 6, 8]
Concurrent code has the same trap whenever threads share data, which is why thread-safe classes such as AtomicInteger, locks and concurrent collections exist. The Java concurrency guide groups them by task.
7. Concurrency vs Parallelism FAQs
Short, direct answers settle the mix-ups between the two terms that we see most often.
7.1. Can we have concurrency without parallelism?
Yes. A single-core machine runs several threads by switching between them, so the tasks overlap in time but never run at the same instant. A server on a one-CPU container is concurrent but not parallel.
7.2. Is multithreading concurrency or parallelism?
Multithreading is a way to write concurrent code. Whether the threads also run in parallel depends on the number of cores and on whether the threads compute or wait.
7.3. Are virtual threads parallel?
Virtual threads are a concurrency tool. They run on carrier threads, by default one per core, so on a multi-core machine several of them do run at the same instant, but their purpose is to let many waiting tasks share few OS threads, not to speed up computation.
7.4. Are parallel streams concurrent?
A parallel stream uses several threads of the common ForkJoinPool at the same time, so it is concurrent in the technical sense, but its purpose is parallelism, i.e. finishing one stream pipeline faster on more cores.
7.5. Which is faster, concurrency or parallelism?
Neither in general, because they improve different metrics. Concurrency raises throughput for waiting tasks, and parallelism lowers latency for CPU-bound jobs. Using the wrong one, such as a parallel stream full of HTTP calls, makes an app slower.
8. Conclusion
Concurrency is about dealing with many tasks during the same period, and it works on one core by interleaving them. Parallelism is about running parts of one job at the same instant on several cores, and it needs the hardware for it.
In Java 25, we get concurrency with threads, executors and virtual threads, and parallelism with parallel streams, ForkJoinPool and pools sized to the cores. The quickest way to choose is to ask whether the tasks wait or compute.
9. References
- Ron Pressler, On Parallelism and Concurrency
- Oracle Java Tutorials, Concurrency
- Oracle Java Tutorials, Parallelism
- ForkJoinPool Javadoc (Java 25)
- JEP 444, Virtual Threads
Happy Learning !!
A program could be run on a multi core or uni core CPU. However if this program is run on a multi core CPU and can make use of more than one core than it has the capability for parallel processing, otherwise it cannot be used for parallel processing even on a multi core CPU
Hi Lokesh,
Thanks for bringing this point to notice. Since Java5 if we are using CachedThreadPool and program is running on multi-core CPU then can we say we are running program parallel.