Concurrency & Threading — Cheat Sheet
Java A–Z · 13 topics. Download the PDF or the Instagram carousel and share it.
Cheat Sheet · AiCanCode.org
Concurrency & Threading
Java A–Z13 topicsQuick revision reference
1
Concurrency Basics
- ✓Prefer Runnable/lambda over extending Thread — separates the task from execution.
- ✓Call start(), not run(), to launch a new thread.
- ✓Thread states: NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED.
- ✓interrupt() sets the interrupt flag; catch InterruptedException and restore the flag.
- ✓Race conditions arise from atomicity, visibility, and ordering violations.
ThreadCreation.java
// 1. Extend Thread (not recommended for modern code)
class MyThread extends Thread {
@Override public void run() {
System.out.println("Running in: " + Thread.currentThread().getName());
}
}
new MyThread().start();
// 2. Implement Runnable (better — separates task from thread)
Runnable task = () -> System.out.println("Task in: "
+ Thread.currentThread().getName());
new Thread(task).start();
// 3. Thread factory with name (Java 19+)
Thread t = Thread.ofPlatform()
.name("worker-", 0)
.start(() -> System.out.println("Named thread"));
// Thread state inspection
Thread main = Thread.currentThread();
System.out.println(main.getName()); // main
System.out.println(main.getState()); // RUNNABLE
System.out.println(main.isDaemon()); // false
System.out.println(main.getPriority()); // 52
synchronized Keyword
- ✓synchronized on instance method locks on this; on static method locks on Class.
- ✓Use a private final lock object instead of this for better encapsulation.
- ✓synchronized provides both mutual exclusion and memory visibility (happens-before).
- ✓Always call wait() in a loop to guard against spurious wakeups.
- ✓Deadlock prevention: acquire locks in a consistent global order, or use tryLock().
SafeCounter.java
public class SafeCounter {
private int count = 0;
private final Object lock = new Object(); // dedicated lock
// Synchronized method — locks on 'this'
public synchronized void increment() {
count++;
}
// Synchronized block — locks on dedicated lock object
public void decrement() {
synchronized (lock) {
count--;
}
}
// Static synchronized — locks on SafeCounter.class
private static int instances = 0;
public static synchronized void registerInstance() {
instances++;
}
public synchronized int get() { return count; }
}
// Now race-condition-free
SafeCounter counter = new SafeCounter();
List<Thread> threads = IntStream.range(0, 1000)
.mapToObj(i -> new Thread(counter::increment))
.collect(Collectors.toList());
threads.forEach(Thread::start);
for (Thread t : threads) t.join();
System.out.println(counter.get()); // Always 10003
volatile Keyword
- ✓volatile guarantees visibility (all threads see the latest value) but NOT atomicity.
- ✓Use volatile for simple status flags and single-assignment fields read by multiple threads.
- ✓volatile does NOT make compound operations (count++) thread-safe — use AtomicInteger.
- ✓volatile is required for double-checked locking to prevent partially-constructed object visibility.
- ✓volatile establishes happens-before: a write happens-before all subsequent reads of that variable.
VolatileVisibility.java
// WITHOUT volatile — loop may never terminate
public class VisibilityBug {
private static boolean stop = false; // no volatile
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
while (!stop) { /* may loop forever — sees cached false */ }
System.out.println("Stopped");
});
worker.start();
Thread.sleep(100);
stop = true; // worker may never see this update
}
}
// WITH volatile — guaranteed visibility
public class VisibilityFixed {
private static volatile boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
while (!stop) {}
System.out.println("Stopped");
});
worker.start();
Thread.sleep(100);
stop = true; // worker WILL see this update
}
}4
ReentrantLock and Locks
- ✓Always unlock in a finally block — otherwise a lock is permanently held on exception.
- ✓tryLock() avoids deadlocks by allowing a thread to back off if the lock is unavailable.
- ✓Multiple Condition objects allow fine-grained wait/signal (unlike synchronized's single wait-set).
- ✓ReadWriteLock: multiple concurrent readers, exclusive writer — great for read-heavy caches.
- ✓StampedLock optimistic reading avoids locking when no concurrent write occurred.
ReentrantLock.java
import java.util.concurrent.locks.*;
public class SafeCounter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // acquire
try {
count++;
} finally {
lock.unlock(); // ALWAYS release in finally
}
}
public int get() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
// tryLock — non-blocking attempt
public boolean tryIncrement() {
if (lock.tryLock()) {
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false; // lock not available
}
}5
Atomic Classes
- ✓Atomic classes use CPU CAS instructions — lock-free and faster than synchronized for single variables.
- ✓incrementAndGet() returns new value; getAndIncrement() returns old value.
- ✓compareAndSet(expected, update) only updates if current == expected — the core of CAS.
- ✓LongAdder is faster than AtomicLong under high contention — uses per-thread cells.
- ✓AtomicStampedReference solves the ABA problem by pairing a value with a version stamp.
AtomicInteger.java
import java.util.concurrent.atomic.*;
AtomicInteger counter = new AtomicInteger(0);
// Basic operations
counter.incrementAndGet(); // 1
counter.decrementAndGet(); // 0
counter.addAndGet(5); // 5
counter.getAndIncrement(); // 5 (returns old, then increments to 6)
int current = counter.get(); // 6
// compareAndSet — CAS: only sets if current == expected
boolean updated = counter.compareAndSet(6, 10); // true
boolean failed = counter.compareAndSet(6, 20); // false (current is 10)
// getAndUpdate / updateAndGet (Java 8+)
counter.updateAndGet(n -> n * 2); // 20
int old = counter.getAndUpdate(n -> n + 100); // 20, counter = 120
// Use in a counter across many threads
AtomicInteger hits = new AtomicInteger(0);
IntStream.range(0, 1000).parallel()
.forEach(i -> hits.incrementAndGet());
System.out.println(hits.get()); // Always 10006
ExecutorService and Thread Pools
- ✓Never create raw threads in production — use an ExecutorService.
- ✓submit() returns Future; execute() is fire-and-forget.
- ✓Always call shutdown() and awaitTermination() to gracefully stop the pool.
- ✓Fixed pool for CPU-bound; cached pool for I/O-bound; scheduled pool for timed tasks.
- ✓ThreadPoolExecutor provides full control over core size, max size, queue, and rejection policy.
ExecutorBasics.java
import java.util.concurrent.*;
// Fixed pool — n threads, unbounded queue
ExecutorService pool = Executors.newFixedThreadPool(4);
// Submit a Runnable (no return value)
pool.execute(() -> System.out.println("Task 1"));
// Submit a Callable (returns a value)
Future<Integer> future = pool.submit(() -> {
Thread.sleep(100);
return 42;
});
// Block until result is ready
int result = future.get(); // 42
int resultWithTimeout = future.get(5, TimeUnit.SECONDS);
// Graceful shutdown
pool.shutdown(); // no new tasks accepted
pool.awaitTermination(10, TimeUnit.SECONDS); // wait for in-flight tasks
// Force shutdown if still running
if (!pool.isTerminated()) pool.shutdownNow();7
Callable and Future
- ✓Callable<V> returns a value and can throw checked exceptions — Runnable cannot.
- ✓Future.get() blocks; use get(timeout, unit) to avoid infinite blocking.
- ✓ExecutionException wraps the exception thrown inside call() — always check getCause().
- ✓invokeAll() waits for all; invokeAny() returns the first success and cancels the rest.
- ✓FutureTask is both a Runnable and a Future — useful for lazy one-time initialisation.
CallableFuture.java
import java.util.concurrent.*;
ExecutorService pool = Executors.newFixedThreadPool(4);
// Callable — returns a value
Callable<String> task = () -> {
Thread.sleep(500);
return "Result from thread: " + Thread.currentThread().getName();
};
Future<String> future = pool.submit(task);
// Do other work while task runs...
System.out.println("Task submitted, doing other work");
// Get result — blocks until ready
try {
String result = future.get(2, TimeUnit.SECONDS);
System.out.println(result);
} catch (TimeoutException e) {
future.cancel(true); // cancel if too slow
System.err.println("Task timed out");
} catch (ExecutionException e) {
System.err.println("Task threw: " + e.getCause());
} catch (CancellationException e) {
System.err.println("Task was cancelled");
}
pool.shutdown();8
CompletableFuture
- ✓supplyAsync() runs async; thenApply() transforms (map); thenCompose() chains (flatMap).
- ✓thenCombine() joins two independent futures; allOf() waits for all; anyOf() takes the first.
- ✓exceptionally() handles errors with a fallback; handle() processes both success and failure.
- ✓join() is like get() but throws unchecked — prefer it in lambda chains.
- ✓Always specify a custom executor for I/O tasks to avoid starving the common ForkJoinPool.
BasicChaining.java
import java.util.concurrent.CompletableFuture;
// Run async, transform result
CompletableFuture<String> cf = CompletableFuture
.supplyAsync(() -> fetchUserFromDb(42)) // async
.thenApply(user -> user.getEmail()) // transform
.thenApply(String::toUpperCase); // transform again
// Non-blocking callback
cf.thenAccept(email ->
System.out.println("Email: " + email));
// Block only at the end if you need the value
String email = cf.join(); // like get() but throws unchecked
// Run with specific executor (avoid hogging common pool)
ExecutorService ioPool = Executors.newFixedThreadPool(10);
CompletableFuture<String> withPool = CompletableFuture
.supplyAsync(() -> callExternalApi(), ioPool)
.thenApplyAsync(response -> parse(response), ioPool);9
Concurrent Collections
- ✓ConcurrentHashMap: high-concurrency map with atomic operations (computeIfAbsent, merge).
- ✓Collections.synchronizedMap() serialises all access — prefer ConcurrentHashMap.
- ✓BlockingQueue: put() blocks when full; take() blocks when empty — natural back-pressure.
- ✓CopyOnWriteArrayList: lock-free reads via array snapshot — good for read-heavy listener lists.
- ✓Never use ArrayList, HashMap, or HashSet across threads without synchronisation.
ConcurrentHashMap.java
import java.util.concurrent.*;
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Thread-safe put/get/remove
map.put("a", 1);
map.get("a");
// Atomic operations — essential for concurrent use
// Only inserts if key absent
map.putIfAbsent("a", 2); // no-op, already exists
// Compute atomically
map.computeIfAbsent("b", k -> k.length()); // b → 1
map.computeIfPresent("b", (k, v) -> v * 2); // b → 2
// Atomic increment (word frequency count)
map.merge("word", 1, Integer::sum);
map.merge("word", 1, Integer::sum); // "word" → 2
// compute — always called (present or absent)
map.compute("counter", (k, v) -> v == null ? 1 : v + 1);
// Bulk operations (Java 8+) — parallel-friendly
map.forEach(2, // parallelism threshold
(k, v) -> System.out.println(k + "=" + v));
int total = map.reduceValues(1, Integer::sum);10
Virtual Threads (Project Loom)
- ✓Virtual threads are JVM-managed, lightweight (~few KB), and you can create millions.
- ✓When a virtual thread blocks on I/O, the JVM unmounts it — the carrier thread is freed.
- ✓Use Executors.newVirtualThreadPerTaskExecutor() to get one virtual thread per task.
- ✓synchronized blocks pin virtual threads to carrier threads — use ReentrantLock for I/O sections.
- ✓Structured Concurrency scopes subtask lifetimes to the parent — prevents thread leaks.
VirtualThreads.java
// Create a single virtual thread
Thread vt = Thread.ofVirtual()
.name("my-virtual-thread")
.start(() -> {
System.out.println("Running in virtual thread: "
+ Thread.currentThread().isVirtual()); // true
});
vt.join();
// Virtual thread executor — one virtual thread per task
try (ExecutorService exec =
Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futures = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
int taskId = i;
futures.add(exec.submit(() -> {
Thread.sleep(100); // blocks, but doesn't tie up OS thread
return "result-" + taskId;
}));
}
// All 10,000 tasks run concurrently without 10,000 OS threads
for (Future<String> f : futures) System.out.println(f.get());
}11
Fork/Join Framework
- ✓RecursiveTask<V> returns a result; RecursiveAction returns void.
- ✓Pattern: if small → solve directly; else fork + join two halves.
- ✓fork() submits a subtask asynchronously; join() blocks until it completes.
- ✓Work-stealing: idle threads steal from tails of busy threads' deques.
- ✓Parallel streams use ForkJoinPool.commonPool() — avoid blocking I/O inside parallel stream operations.
RecursiveTask.java
import java.util.concurrent.*;
// RecursiveTask — returns a result
public class ParallelSum extends RecursiveTask<Long> {
private static final int THRESHOLD = 10_000;
private final long[] array;
private final int start, end;
public ParallelSum(long[] array, int start, int end) {
this.array = array; this.start = start; this.end = end;
}
@Override
protected Long compute() {
int length = end - start;
if (length <= THRESHOLD) {
// Base case — sequential
long sum = 0;
for (int i = start; i < end; i++) sum += array[i];
return sum;
}
// Divide
int mid = start + length / 2;
ParallelSum left = new ParallelSum(array, start, mid);
ParallelSum right = new ParallelSum(array, mid, end);
left.fork(); // schedule left async
long rightResult = right.compute(); // run right inline
long leftResult = left.join(); // wait for left
return leftResult + rightResult; // combine
}
}
// Run it
ForkJoinPool pool = ForkJoinPool.commonPool();
long[] data = LongStream.range(0, 1_000_000).toArray();
long total = pool.invoke(new ParallelSum(data, 0, data.length));12
CompletableFuture — Advanced Patterns
- ✓Use separate thread pools per external dependency (bulkhead) to prevent cascade failures.
- ✓completeOnTimeout() provides a fallback value on timeout; orTimeout() completes exceptionally.
- ✓delayedExecutor() schedules future execution without blocking a thread.
- ✓Retry with exponential backoff + jitter prevents thundering herd on service recovery.
- ✓Fan-in with partial results: use exceptionally(ex -> null) to collect successes only.
Bulkhead.java
// Separate executors per external dependency (bulkhead)
ExecutorService dbPool = Executors.newFixedThreadPool(20);
ExecutorService paymentPool = Executors.newFixedThreadPool(5);
ExecutorService emailPool = Executors.newVirtualThreadPerTaskExecutor();
public CompletableFuture<OrderResult> placeOrderAsync(OrderRequest req) {
CompletableFuture<User> userFuture =
CompletableFuture.supplyAsync(() -> userDb.find(req.userId()), dbPool);
CompletableFuture<Payment> payFuture =
CompletableFuture.supplyAsync(() -> payment.charge(req), paymentPool);
return userFuture.thenCombineAsync(payFuture, (user, pay) -> {
Order order = orderDb.save(new Order(user, pay));
// Email on separate pool — doesn't block order completion
CompletableFuture.runAsync(
() -> emailService.sendConfirmation(user, order), emailPool);
return new OrderResult(order.id(), pay.txnId());
}, dbPool); // combine result on DB pool
}13
Structured Concurrency
- ✓StructuredTaskScope guarantees all subtasks finish when the scope closes — no thread leaks.
- ✓ShutdownOnFailure: first failure cancels all siblings and re-throws.
- ✓ShutdownOnSuccess: first success cancels all siblings and returns the result.
- ✓joinUntil(Instant) provides deadline-based waiting.
- ✓Structured Concurrency makes thread relationships visible to debuggers and profilers.
StructuredTaskScope.java
import java.util.concurrent.*;
record UserProfile(User user, List<Order> orders, AccountStatus status) {}
UserProfile fetchProfile(long userId) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// Fork three concurrent fetches
Subtask<User> userTask = scope.fork(() -> userDb.find(userId));
Subtask<List<Order>> ordersTask = scope.fork(() -> orderDb.findByUser(userId));
Subtask<AccountStatus> statusTask = scope.fork(() -> accountSvc.getStatus(userId));
scope.join() // wait for all three
.throwIfFailed(); // if any threw, re-throw here
// All succeeded — get results
return new UserProfile(
userTask.get(),
ordersTask.get(),
statusTask.get()
);
}
// Scope closed: all subtasks guaranteed complete
// If any failed: remaining were cancelled, exception propagated
}Learn this free with Aria, your AI tutor → AiCanCode.org/learn/java