Lesson 11 of 13

Threads, Executors & Concurrency

Learn how to run work in parallel with threads, executors and Java 21 virtual threads, and how to share data between threads safely.

Java
import java.util.concurrent.*;
import java.util.*;

public class Concurrency {
    public static void main(String[] args) throws Exception {
        // Do not create raw Threads by hand — use an executor pool.
        ExecutorService pool = Executors.newFixedThreadPool(4);

        List<Callable<Integer>> jobs = new ArrayList<>();
        for (int i = 1; i <= 8; i++) {
            final int n = i;
            jobs.add(() -> { Thread.sleep(100); return n * n; });
        }

        List<Future<Integer>> futures = pool.invokeAll(jobs);
        int total = 0;
        for (Future<Integer> f : futures) total += f.get();   // get() blocks
        System.out.println("total = " + total);               // 204

        pool.shutdown();
        pool.awaitTermination(5, TimeUnit.SECONDS);

        // Java 21+: virtual threads — millions of them, cheap to block
        try (var vt = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10_000; i++) {
                vt.submit(() -> { Thread.sleep(1000); return null; });
            }
        }   // close() waits for all tasks
    }
}
Java
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

// The classic bug: two threads incrementing the same field.
class Counter {
    private int count = 0;
    void increment() { count++; }              // NOT atomic: read, add, write
    int get() { return count; }
}

// Three correct fixes:
class SyncCounter {
    private int count = 0;
    synchronized void increment() { count++; }        // 1. lock the method
    synchronized int get() { return count; }
}

class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger();
    void increment() { count.incrementAndGet(); }     // 2. atomic primitive
    int get() { return count.get(); }
}

class LockCounter {
    private final java.util.concurrent.locks.ReentrantLock lock =
        new java.util.concurrent.locks.ReentrantLock();
    private int count = 0;
    void increment() {                                 // 3. explicit lock
        lock.lock();
        try { count++; } finally { lock.unlock(); }    // ALWAYS unlock in finally
    }
}

// Concurrent collections instead of manual locking
ConcurrentHashMap<String, Integer> hits = new ConcurrentHashMap<>();
hits.merge("/home", 1, Integer::sum);
BlockingQueue<String> queue = new LinkedBlockingQueue<>(100);
Java
import java.util.concurrent.*;

// CompletableFuture: composable async work without blocking on get()
CompletableFuture<String> user   = CompletableFuture.supplyAsync(() -> fetchUser(42));
CompletableFuture<String> orders = CompletableFuture.supplyAsync(() -> fetchOrders(42));

CompletableFuture<String> combined = user
    .thenCombine(orders, (u, o) -> u + " has " + o)
    .thenApply(String::toUpperCase)
    .exceptionally(err -> "failed: " + err.getMessage())
    .orTimeout(3, TimeUnit.SECONDS);

System.out.println(combined.join());

// Chain dependent calls with thenCompose (flatMap for futures)
CompletableFuture<Integer> pipeline =
    CompletableFuture.supplyAsync(() -> 21)
        .thenCompose(n -> CompletableFuture.supplyAsync(() -> n * 2));

static String fetchUser(int id)   { return "user" + id; }
static String fetchOrders(int id) { return "3 orders"; }
Finished reading?Mark it done to track your progress.

Tip: use the ← and → keys to move between lessons.