Site Search:

Java concurrency in practice with examples

This java study materials arranged around Brian Goetz's book Java concurrency in practice, added java 7 and 8 features. Runnable java examples are provided for each chapters.

Chapter 1 Introduction

I Fundamentals


Chapter 2 Thread Safety

Chapter 3 Sharing Objects

Chapter 4 Composing Objects

Chapter 5 Building blocks

II Structuring Concurrent Applications




Chapter 9 GUI Applications

III Liveness, Performance, and Testing




Concurrency in Java 8 (lamda and parallel Stream)







Python asyncio vs Java CompletableFuture: Parallel Composition & Chaining

Back>


Understanding Java Future, CompletableFuture, and Python asyncio Coroutines

Asynchronous programming helps developers build responsive, scalable applications. In Java, two major abstractions—Future and CompletableFuture—are commonly used. In Python, the asyncio framework uses coroutines and an event loop to handle async logic efficiently.

What is a Java Future?

A Future<T> is a handle to a result that may not yet be computed. It is returned when submitting a task to an ExecutorService and provides methods like get() to block until the result is ready.

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> "hello");
String result = future.get(); // blocks until task completes

While useful, Future is limited: it is blocking and doesn’t support fluent composition of tasks.

What is Java CompletableFuture?

CompletableFuture was introduced in Java 8 to extend Future with non-blocking, composable operations. It allows chaining logic with methods like thenApply, thenAccept, and thenCombine.

CompletableFuture.supplyAsync(() -> "hello")
    .thenApply(s -> s + " world")
    .thenAccept(System.out::println);

CompletableFuture often uses a thread pool (such as ForkJoinPool.commonPool()) and supports both I/O-bound and CPU-bound workloads.

What are Python Coroutines?

A coroutine is a special kind of function defined with async def that can pause and resume its execution using await. Coroutines are scheduled by an event loop—a central scheduler that handles when and how coroutines run.

import asyncio

async def fetch_data():
    await asyncio.sleep(1)
    return "data"

async def main():
    result = await fetch_data()
    print(result)

asyncio.run(main())

Unlike threads, coroutines use cooperative multitasking. They voluntarily yield control at await points, allowing the event loop to run other coroutines. This makes them highly efficient for I/O-bound tasks.

Comparing Java Futures and Python Coroutines

When running multiple asynchronous tasks concurrently and combining their results, both Python's asyncio and Java's CompletableFuture offer elegant solutions. Here's how they compare in behavior and syntax.

Visual Timeline

Below is a visual timeline comparing the execution flow of two tasks and a chained result:


Python asyncio (Top Blue Bars): Uses an event loop to switch between coroutines (fetch_a, then fetch_b) — cooperative and sequential in appearance, even though they're both non-blocking.

Java CompletableFuture (Bottom Green & Orange Bars): Uses threads to run tasks (futureA and futureB) truly in parallel, thanks to thread pool execution.



Top (Blue + Purple)asyncio.gather() runs fetch_a() and fetch_b() concurrently, then awaits a final step to combine results.

Bottom (Green + Orange + Red)CompletableFuture.allOf() runs futureA and futureB in parallel, then uses thenCombine() to process the combined result asynchronously.


Python asyncio.gather() + chaining

import asyncio

async def fetch_a():
    await asyncio.sleep(1)
    return "A"

async def fetch_b():
    await asyncio.sleep(1)
    return "B"

async def main():
    result_a, result_b = await asyncio.gather(fetch_a(), fetch_b())
    combined = f"{result_a} + {result_b}"
    print("Result:", combined)

asyncio.run(main())

Java CompletableFuture.allOf() + thenCombine()

import java.util.concurrent.*;

public class CombinedAsync {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        CompletableFuture<String> futureA = CompletableFuture.supplyAsync(() -> {
            sleep(1000);
            return "A";
        }, executor);

        CompletableFuture<String> futureB = CompletableFuture.supplyAsync(() -> {
            sleep(1000);
            return "B";
        }, executor);

        futureA.thenCombine(futureB, (a, b) -> a + " + " + b)
               .thenAccept(System.out::println)
               .join();

        executor.shutdown();
    }

    private static void sleep(long millis) {
        try { Thread.sleep(millis); } catch (InterruptedException ignored) {}
    }
}

Takeaway

  • Both models enable parallel async composition — Python via asyncio.gather, Java via thenCombine.
  • Python coroutines are cooperatively scheduled on a single-threaded event loop.
  • Java futures run on actual threads in a pool, allowing true multi-core execution.

Both approaches are powerful. Python asyncio is good for IO-heavy single-threaded tasks, and Java CompletableFuture gives you true parallelism or more flexible composition with error handling.

Concept Java Future Java CompletableFuture Python asyncio + coroutines
Core abstraction Future Future + reactive chaining Coroutine (async def)
Execution model Thread pool (manual) Thread pool (default async) Single-threaded event loop

At the time of this blog post, in the area of Asynchronous programming, Java is the winner. As virtual threads become permanent feature since JDK 21, it is trending to move away from async and reative programming.



Chapter 12 Testing Concurrent Programs

Back>

Testing concurrent programs is a critical aspect of software development. It ensures that multithreaded applications behave correctly and perform efficiently under various workloads. Chapter 12 of Java Concurrency in Practice outlines the essential strategies for testing concurrent systems—from correctness to performance and scalability—and highlights the risks and tools involved in doing so.

Testing for Correctness

Correctness testing focuses on ensuring that a concurrent program does not suffer from data races, deadlocks, or unexpected behavior due to thread interactions. These are broadly divided into:

  • Safety tests: Ensure the program never enters an incorrect state.
  • Liveness tests: Ensure the program continues to make progress and does not hang or livelock.

A good example is the SemaphoreBoundedBuffer, which uses semaphores to manage concurrent access to a bounded buffer. The accompanying test classes demonstrate how to validate both blocking and non-blocking behaviors. These tests include checking that:

  • The buffer blocks on take when empty
  • The buffer blocks on put when full
  • The buffer does not leak memory or fail under repeated usage

Performance Testing: Throughput, Responsiveness, and Scalability

Beyond correctness, performance testing addresses how the program behaves under load. We usually test for:

  • Throughput: How many operations the program can complete in a given time.
  • Responsiveness: How quickly the system responds to requests.
  • Scalability: How performance changes as more threads or cores are added.

In TestThreadPool, we simulate load against a thread pool and observe how it performs under concurrent task submission. This helps evaluate how well the thread pool scales, responds under pressure, and maintains throughput over time.

Avoiding Performance Testing Pitfalls

Performance testing concurrent code is difficult due to the inherent non-determinism of thread scheduling and execution. Common pitfalls include:

  • Non-reproducible results due to thread timing and race conditions.
  • Test interference from other processes or testing frameworks.
  • Measuring the test harness instead of the system under test.

To mitigate these issues:

  • Run tests in isolation with minimal background load.
  • Repeat tests multiple times and take median results.
  • Profile system metrics like CPU, memory, and GC to detect skew.

Modern Techniques and Tools for 2025

In 2025, developers have access to more advanced tools and practices that make testing concurrent programs more practical and reliable:

  • AI-Driven Test Case Generation – AI tools analyze code and generate concurrent test cases.
    Search keyword: AI test generation concurrency
  • Shift-Left Concurrency Testing – Integrate concurrency testing early in the CI/CD pipeline.
    Search keyword: shift-left concurrency testing
  • LitmusKt – A Kotlin-based concurrency stress-testing framework inspired by litmus tests.
    Search keyword: LitmusKt concurrency testing
  • JMH (Java Microbenchmark Harness) – The go-to tool for writing low-overhead, precision Java benchmarks.
    Search keyword: JMH concurrency benchmarks
  • Python: pytest + asyncio – For testing async code with event loops and timeouts.
    Search keyword: pytest asyncio concurrency testing

Conclusion

Testing concurrent programs is a combination of art and engineering. It starts with unit tests that verify correctness, followed by performance tests that measure how systems behave under stress. With modern tools and thoughtful test design, we can build systems that are not only safe and correct but also scalable and responsive under real-world conditions.

In the next chapter, we’ll explore explicit locking and conditions—and how to use them safely and effectively when high-level abstractions aren’t enough.

TestThreadPool.java

 TestThreadPool.java

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import static org.junit.Assert.*;

/**
* TestingThreadFactory
* <p/>
* Testing thread pool expansion
*
*/
public class TestThreadPool {

private final TestingThreadFactory threadFactory = new TestingThreadFactory();
public static void main(String...args) throws InterruptedException {
TestThreadPool ttp = new TestThreadPool();
ttp.testPoolExpansion();
}
public void testPoolExpansion() throws InterruptedException {
int MAX_SIZE = 10;
//ExecutorService exec = Executors.newFixedThreadPool(MAX_SIZE);
ExecutorService exec = new ThreadPoolExecutor(
MAX_SIZE, MAX_SIZE, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory // Use the TestingThreadFactory here
);

for (int i = 0; i < 100 * MAX_SIZE; i++)
exec.execute(new Runnable() {
public void run() {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
for (int i = 0;
i < 20 && threadFactory.numCreated.get() < MAX_SIZE;
i++)
Thread.sleep(100);
assertEquals(threadFactory.numCreated.get(), MAX_SIZE);
exec.shutdownNow();
}
}

class TestingThreadFactory implements ThreadFactory {
public final AtomicInteger numCreated = new AtomicInteger();
private final ThreadFactory factory = Executors.defaultThreadFactory();

public Thread newThread(Runnable r) {
numCreated.incrementAndGet();
return factory.newThread(r);
}
}

This test uses a TestingThreadFactory that maintains a count of created threads, so that test cases can verify the number of threads created during a test run.