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.
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
}
}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);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.