Skip to main content

Concurrency

Concurrency allows your program to do multiple things simultaneously. It’s essential for modern applications to utilize multi-core processors and handle multiple users.

1. Threads vs. Processes

  • Process: An executing program (e.g., the JVM itself). It has its own isolated memory space.
  • Thread: A lightweight unit of execution within a process. Threads share the same memory space.
Think of a Process like a house, and Threads like the people living in it. They share the kitchen (memory), but do different tasks.

Creating a Thread

// 1. Extend Thread (Not recommended)
class MyThread extends Thread {
    public void run() {
        System.out.println("Running in thread");
    }
}
new MyThread().start();

// 2. Implement Runnable (Preferred)
// Decouples the task from the thread mechanism
Runnable task = () -> System.out.println("Running task");
new Thread(task).start();

2. Executor Framework

Manually creating threads (new Thread()) is expensive and error-prone. If you create 10,000 threads, you might crash the OS. Executors manage a pool of threads for you. You just submit tasks, and the pool handles the execution.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

// Create a pool with 10 fixed threads
ExecutorService executor = Executors.newFixedThreadPool(10);

for (int i = 0; i < 100; i++) {
    executor.submit(() -> {
        System.out.println("Processing task on " + Thread.currentThread().getName());
    });
}

// Shutdown when done (otherwise the app keeps running)
executor.shutdown();

Types of Pools

  • newFixedThreadPool(n): Fixed number of threads. Good for predictable loads.
  • newCachedThreadPool(): Creates threads as needed, reuses idle ones. Good for many short-lived tasks.
  • newSingleThreadExecutor(): One thread. Ensures tasks run sequentially.

3. Callable & Future

Runnable returns void. What if you want a result? Use Callable. A Future represents the result of an asynchronous computation. It’s a placeholder for a value that will arrive later.
import java.util.concurrent.*;

Callable<Integer> task = () -> {
    Thread.sleep(1000); // Simulate work
    return 42;
};

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(task);

// Do other work here...

// Get result (blocks until ready)
Integer result = future.get(); 
System.out.println(result); // 42

4. Synchronization

When multiple threads access shared data (like a counter), race conditions occur. Two threads might read “5”, increment it, and both write “6”, losing one increment.

synchronized Keyword

Ensures only one thread can execute a block at a time. It’s like a lock on a door.
class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
}

Atomic Classes

For simple variables, AtomicInteger is faster and lock-free.
import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // Thread-safe increment

5. CompletableFuture (Java 8+)

Future.get() blocks the thread. CompletableFuture allows you to build non-blocking, reactive pipelines (similar to Promises in JavaScript).
CompletableFuture.supplyAsync(() -> "Hello")
    .thenApply(s -> s + " World")
    .thenAccept(System.out::println); // Prints "Hello World"
    
// Combine two futures
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "A");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "B");

f1.thenCombine(f2, (a, b) -> a + b)
  .thenAccept(System.out::println); // "AB"

6. Virtual Threads (Java 21)

Game Changer: Traditional threads are mapped 1:1 to OS threads. They are heavy (2MB stack). You can only have a few thousand. Virtual Threads are lightweight threads managed by the JVM. You can create millions of them.
// Create a virtual thread
Thread.startVirtualThread(() -> {
    System.out.println("Running on virtual thread");
});

// Executor for virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
} // Auto-closes and waits for all tasks
Why Virtual Threads? They make blocking I/O cheap. You can write simple, synchronous code (e.g., read from DB) that scales like complex asynchronous code.

Summary

  • Threads: Basic unit of concurrency.
  • Executors: Manage thread pools.
  • Synchronization: Protect shared state from race conditions.
  • CompletableFuture: Compose async tasks.
  • Virtual Threads: High-throughput concurrency for Java 21+.
Next, we’ll wrap up with Modern Java Features that make the language expressive and concise.