Cheat SheetsJava A–ZConcurrency & Threading

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()); // 5
2

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 1000
3

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 1000
6

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