10 - Multithreading and Virtual Threads
A complete beginner-to-advanced guide to Java concurrency, aligned with the Oracle Certified Professional: Java SE 21 Developer (1Z0-830) exam objectives. Thorough coverage of Java 21 Virtual Threads.
Table of Contents
- Concurrency Basics
- Creating Threads
- Thread Lifecycle
- Thread Methods
- Race Conditions
- Synchronization
- Locks
- Deadlocks
- The Executor Framework
- Runnable vs Callable
- Future
- CompletableFuture
- Concurrent Collections
- Atomic Classes
- Virtual Threads (Java 21)
- Platform vs Virtual Threads
- Certification Traps
- Common Mistakes
- Interview Questions
- Quick Revision Notes
- One-Page Cheat Sheet
1. Concurrency Basics
Concurrency means dealing with many tasks at once. A thread is the smallest unit of execution. Multiple threads in one program run (seemingly) simultaneously.
| Term | Meaning |
|---|---|
| Process | An independent running program with its own memory. |
| Thread | A lightweight unit of execution inside a process. |
| Concurrency | Managing multiple tasks (may interleave on one core). |
| Parallelism | Tasks literally running at the same time (multiple cores). |
| Main thread | The thread that runs main(). |
Process (JVM)
+-------------------------------+
| Thread 1 Thread 2 Thread 3| <- share heap memory
| (main) (worker) (worker)|
+-------------------------------+
Benefit: responsiveness and throughput. Cost: complexity — shared data must be coordinated.
2. Creating Threads
Way 1: Extend Thread
class MyThread extends Thread {
public void run() {
System.out.println("Running: " + Thread.currentThread().getName());
}
}
new MyThread().start(); // start() spawns a new thread
Way 2: Implement Runnable (preferred)
class MyTask implements Runnable {
public void run() {
System.out.println("Task running");
}
}
new Thread(new MyTask()).start();
// With a lambda (Runnable is a functional interface):
new Thread(() -> System.out.println("Lambda task")).start();
start() vs run() (Critical Trap)
Thread t = new Thread(() -> System.out.println("hi"));
t.run(); // runs in the CURRENT thread (no new thread!)
t.start(); // runs in a NEW thread
start()creates a new thread and callsrun()on it. Callingrun()directly executes in the current thread — no concurrency. Callingstart()twice throwsIllegalThreadStateException.
3. Thread Lifecycle
A thread moves through well-defined states (Thread.State enum).
start() scheduler picks
NEW --------------> RUNNABLE <-------------> RUNNING
^ | \
notify()/ | | \ sleep()/wait()/join()/lock
lock acquired | | \
| | v
BLOCKED/WAITING/TIMED_WAITING
|
run() completes
v
TERMINATED
| State | Meaning |
|---|---|
NEW | Created but start() not yet called. |
RUNNABLE | Ready to run or running (scheduler decides). |
BLOCKED | Waiting to acquire a monitor lock. |
WAITING | Waiting indefinitely (wait(), join()). |
TIMED_WAITING | Waiting with a timeout (sleep(ms), wait(ms)). |
TERMINATED | Finished execution. |
Thread t = new Thread(() -> {});
System.out.println(t.getState()); // NEW
t.start();
// ... RUNNABLE -> TERMINATED
4. Thread Methods
| Method | Effect |
|---|---|
start() | Begin execution in a new thread. |
run() | The task body (don't call directly). |
sleep(ms) | Pause current thread (keeps locks). |
join() | Wait for another thread to finish. |
interrupt() | Request a thread to stop. |
setDaemon(true) | Make it a background (daemon) thread. |
setPriority(n) | Hint scheduler priority (1–10). |
isAlive() | Whether the thread is still running. |
Thread worker = new Thread(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) { }
System.out.println("done");
});
worker.start();
worker.join(); // main waits here until worker finishes
System.out.println("after join");
Daemon Threads
Thread daemon = new Thread(() -> { while (true) {} });
daemon.setDaemon(true); // JVM won't wait for daemon threads to exit
daemon.start();
Trap:
sleep()holds any locks it has;wait()releases the lock.setDaemon()must be called beforestart().
5. Race Conditions
A race condition occurs when multiple threads access shared mutable data concurrently and the result depends on timing.
class Counter {
int count = 0;
void increment() { count++; } // NOT atomic: read, add, write
}
Counter c = new Counter();
Runnable task = () -> { for (int i = 0; i < 10000; i++) c.increment(); };
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(c.count); // often LESS than 20000 — lost updates!
Why? count++ Is Three Steps
Thread A reads count = 5
Thread B reads count = 5 <- both read the same value
Thread A writes 6
Thread B writes 6 <- one increment LOST
The fix is synchronization or atomic operations (below).
6. Synchronization
synchronized ensures only one thread at a time executes a critical section, using an object's intrinsic monitor lock.
Synchronized Method
class Counter {
private int count = 0;
public synchronized void increment() { count++; } // thread-safe now
public synchronized int get() { return count; }
}
Synchronized Block (finer-grained)
class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) { // lock only this section
count++;
}
}
}
Key Concepts
| Concept | Detail |
|---|---|
| Monitor lock | Each object has one; synchronized acquires it. |
| Static synchronized | Locks on the class object (Counter.class). |
volatile | Guarantees visibility (not atomicity) of a field. |
| happens-before | Memory model rule ensuring writes are visible to other threads. |
volatile
class Flag {
private volatile boolean running = true; // visible across threads
void stop() { running = false; }
void loop() { while (running) { /* ... */ } } // sees updates
}
Trap:
volatileensures visibility but not atomicity —volatile int x; x++is still a race condition. Usesynchronizedor atomics for compound actions.
wait / notify
synchronized (lock) {
while (!condition) lock.wait(); // releases lock, waits
// ...
lock.notifyAll(); // wakes waiting threads
}
wait,notify,notifyAllmust be called inside asynchronizedblock on the same object, or you getIllegalMonitorStateException.
7. Locks
The java.util.concurrent.locks package offers more flexible locking than synchronized.
ReentrantLock
import java.util.concurrent.locks.*;
class Counter {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // ALWAYS unlock in finally
}
}
}
Lock vs synchronized
| Feature | synchronized | ReentrantLock |
|---|---|---|
| Acquire/release | Automatic | Manual (lock/unlock) |
| Try without blocking | ❌ | ✅ tryLock() |
| Interruptible | ❌ | ✅ lockInterruptibly() |
| Fairness option | ❌ | ✅ new ReentrantLock(true) |
| Multiple conditions | ❌ | ✅ newCondition() |
ReadWriteLock
ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock(); // many readers allowed concurrently
// ...
rwLock.readLock().unlock();
rwLock.writeLock().lock(); // exclusive for writers
// ...
rwLock.writeLock().unlock();
Trap: Always release a lock in a
finallyblock, or an exception can leave it permanently locked.
8. Deadlocks
A deadlock occurs when two or more threads each hold a lock the other needs, so none can proceed.
// Thread 1: locks A then B
synchronized (A) { synchronized (B) { } }
// Thread 2: locks B then A <- opposite order = deadlock risk
synchronized (B) { synchronized (A) { } }
Thread 1 holds A, wants B ----+
| circular wait = DEADLOCK
Thread 2 holds B, wants A ----+
The Four Coffman Conditions (all required)
| Condition | Meaning |
|---|---|
| Mutual exclusion | Resource held by one thread. |
| Hold and wait | Holds one, waits for another. |
| No preemption | Locks can't be forcibly taken. |
| Circular wait | A cycle of waiting threads. |
Prevention
| Strategy | Detail |
|---|---|
| Lock ordering | Always acquire locks in the same global order. |
tryLock with timeout | Back off if you can't get all locks. |
| Reduce lock scope | Hold locks for the shortest time. |
Related: livelock (threads keep reacting, no progress) and starvation (a thread never gets CPU/lock time).
9. The Executor Framework
Manually creating threads is expensive and hard to manage. The Executor Framework provides thread pools that reuse threads.
import java.util.concurrent.*;
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> System.out.println("Task running"));
executor.shutdown(); // graceful: finish submitted tasks, no new ones
Factory Methods
| Factory | Pool Type |
|---|---|
newFixedThreadPool(n) | Fixed number of threads. |
newCachedThreadPool() | Grows/shrinks as needed. |
newSingleThreadExecutor() | One worker, sequential tasks. |
newScheduledThreadPool(n) | For delayed/periodic tasks. |
newVirtualThreadPerTaskExecutor() | One virtual thread per task (Java 21). |
Shutdown Methods
| Method | Behavior |
|---|---|
shutdown() | Stops accepting tasks; finishes existing ones. |
shutdownNow() | Attempts to stop all immediately. |
awaitTermination(t, unit) | Blocks until done or timeout. |
ScheduledExecutorService
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
// Run once after a delay:
scheduler.schedule(() -> System.out.println("delayed"), 2, TimeUnit.SECONDS);
// Run repeatedly:
scheduler.scheduleAtFixedRate(() -> System.out.println("tick"),
0, 1, TimeUnit.SECONDS);
Trap: If you never call
shutdown(), the JVM may not exit (non-daemon pool threads keep it alive).
10. Runnable vs Callable
| Feature | Runnable | Callable<V> |
|---|---|---|
| Method | run() | call() |
| Returns a value | ❌ No (void) | ✅ Yes (V) |
| Throws checked exception | ❌ No | ✅ Yes |
| Used with | Thread, execute, submit | submit, invokeAll |
| Since | Java 1.0 | Java 5 |
Runnable runnable = () -> System.out.println("no result");
Callable<Integer> callable = () -> {
Thread.sleep(100);
return 42; // returns a value, can throw
};
ExecutorService ex = Executors.newSingleThreadExecutor();
Future<Integer> future = ex.submit(callable);
System.out.println(future.get()); // 42
ex.shutdown();
11. Future
A Future<V> represents the result of an asynchronous computation that may not be ready yet.
ExecutorService ex = Executors.newFixedThreadPool(2);
Future<Integer> future = ex.submit(() -> {
Thread.sleep(1000);
return 100;
});
System.out.println(future.isDone()); // false (probably)
Integer result = future.get(); // BLOCKS until ready -> 100
ex.shutdown();
Future Methods
| Method | Purpose |
|---|---|
get() | Block until result (throws on timeout/cancel). |
get(timeout, unit) | Block with a timeout. |
isDone() | Whether computation finished. |
cancel(boolean) | Attempt to cancel. |
isCancelled() | Whether it was cancelled. |
// invokeAll runs many Callables and returns a list of Futures:
List<Callable<Integer>> tasks = List.of(() -> 1, () -> 2, () -> 3);
List<Future<Integer>> results = ex.invokeAll(tasks);
Trap:
future.get()blocks the calling thread until the result is available. Wrap in a timeout to avoid hanging forever.
12. CompletableFuture
CompletableFuture (Java 8+) supports non-blocking, chainable async pipelines — far more powerful than Future.
CompletableFuture.supplyAsync(() -> 10)
.thenApply(n -> n * 2) // transform: 20
.thenApply(n -> n + 5) // transform: 25
.thenAccept(System.out::println); // consume: prints 25
Key Methods
| Method | Purpose |
|---|---|
supplyAsync(Supplier) | Start async task returning a value. |
runAsync(Runnable) | Start async task with no result. |
thenApply(fn) | Transform the result. |
thenAccept(consumer) | Consume the result. |
thenCompose(fn) | Chain another future (flatMap). |
thenCombine(other, fn) | Combine two futures. |
exceptionally(fn) | Handle errors. |
join() / get() | Wait for the result. |
// Combining two async tasks:
CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> b = CompletableFuture.supplyAsync(() -> 20);
a.thenCombine(b, Integer::sum)
.thenAccept(sum -> System.out.println("Sum: " + sum)); // 30
// Error handling:
CompletableFuture.supplyAsync(() -> { throw new RuntimeException("fail"); })
.exceptionally(ex -> -1)
.thenAccept(System.out::println); // -1
thenApplyvsthenCompose:thenApplymaps to a value;thenComposeflattens a returnedCompletableFuture(avoids nesting).
13. Concurrent Collections
Standard collections (ArrayList, HashMap) are not thread-safe. The java.util.concurrent package provides safe alternatives.
| Collection | Thread-Safe Alternative To | Notes |
|---|---|---|
ConcurrentHashMap | HashMap | Segment/bucket-level locking; high throughput. |
CopyOnWriteArrayList | ArrayList | Copies on write; great for many reads, few writes. |
ConcurrentLinkedQueue | LinkedList queue | Non-blocking FIFO. |
BlockingQueue (ArrayBlockingQueue, LinkedBlockingQueue) | — | Producer-consumer; blocks when full/empty. |
ConcurrentSkipListMap | TreeMap | Sorted, concurrent. |
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.merge("a", 10, Integer::sum); // atomic update -> 11
// Producer-consumer with BlockingQueue:
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
queue.put(1); // blocks if full
int item = queue.take(); // blocks if empty
Trap:
ConcurrentHashMapdoes not allownullkeys or values (unlikeHashMap). UseCollections.synchronizedMaponly for simple needs — it locks the whole map.
14. Atomic Classes
Atomic classes in java.util.concurrent.atomic provide lock-free, thread-safe operations on single variables using CPU compare-and-swap (CAS).
import java.util.concurrent.atomic.*;
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // atomic ++ -> 1
counter.addAndGet(5); // atomic += 5 -> 6
counter.compareAndSet(6, 10); // if 6, set to 10
System.out.println(counter.get()); // 10
| Class | Use |
|---|---|
AtomicInteger | Thread-safe int. |
AtomicLong | Thread-safe long. |
AtomicBoolean | Thread-safe boolean. |
AtomicReference<T> | Thread-safe object reference. |
Fixing the Race Condition
class Counter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); } // no lock needed
public int get() { return count.get(); }
}
// Now two threads x 10000 -> exactly 20000
Why atomics? They avoid lock overhead for simple counters/flags using hardware CAS — faster than
synchronizedfor single-variable updates.
15. Virtual Threads (Java 21)
Virtual threads (Project Loom, finalized in Java 21) are lightweight threads managed by the JVM, not the OS. You can run millions of them cheaply.
The Problem They Solve
Traditional platform threads map 1:1 to OS threads (~1 MB stack each). Blocking thousands of them (e.g., waiting on I/O) is expensive. Virtual threads are cheap and unmount from their carrier thread when they block.
Creating Virtual Threads
// Way 1: directly
Thread vt = Thread.ofVirtual().start(() -> System.out.println("virtual!"));
vt.join();
// Way 2: unstarted
Thread t = Thread.ofVirtual().unstarted(() -> {});
t.start();
// Way 3: factory / executor (recommended)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> System.out.println("task"));
} // try-with-resources auto-closes (waits for tasks)
// Way 4: builder with name
Thread.ofVirtual().name("worker-", 0).start(() -> {});
Millions of Threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
executor.submit(() -> {
Thread.sleep(1000); // cheap to block!
return null;
});
}
} // all 1,000,000 virtual threads run efficiently
How They Work
Many VIRTUAL threads
v1 v2 v3 v4 v5 ... v1000000
| mounted on
v
Few CARRIER (platform) threads (ForkJoinPool)
p1 p2 p3 p4 <- = number of CPU cores
When a virtual thread BLOCKS (I/O, sleep),
it UNMOUNTS, freeing the carrier for another virtual thread.
Pinning (Trap)
A virtual thread is pinned (cannot unmount) when it blocks inside a synchronized block or a native call. This reduces scalability.
// AVOID: synchronized pins the carrier thread
synchronized (lock) {
Thread.sleep(1000); // pins! carrier is blocked
}
// PREFER: ReentrantLock does NOT pin
lock.lock();
try { Thread.sleep(1000); } finally { lock.unlock(); }
Key: Prefer
ReentrantLockoversynchronizedin code that runs on virtual threads to avoid pinning.
16. Platform vs Virtual Threads
| Feature | Platform Thread | Virtual Thread |
|---|---|---|
| Managed by | OS | JVM |
| Mapping | 1:1 with OS thread | Many:few (multiplexed) |
| Stack size | Large (~1 MB) | Small, growable (heap) |
| Count feasible | Thousands | Millions |
| Creation cost | Expensive | Very cheap |
| Best for | CPU-bound work | I/O-bound, high-concurrency |
| Pooling | Recommended | Not needed (create per task) |
| Blocking cost | High (OS thread idle) | Low (unmounts carrier) |
Thread.ofVirtual() | n/a | Creates one |
Thread platform = Thread.ofPlatform().start(() -> {}); // OS thread
Thread virtual = Thread.ofVirtual().start(() -> {}); // JVM thread
System.out.println(virtual.isVirtual()); // true (Java 21)
When to Use Which
| Use Platform Threads | Use Virtual Threads |
|---|---|
| CPU-intensive computation | Many blocking I/O operations |
| Limited, long-running tasks | Thousands of concurrent requests |
| Need thread affinity | Server handling many connections |
Trap: Do not pool virtual threads — they're cheap, so create one per task (
newVirtualThreadPerTaskExecutor). Pooling them defeats their purpose.
17. Certification Traps
| # | Trap |
|---|---|
| 1 | run() runs in the current thread; only start() spawns a new one. |
| 2 | Calling start() twice → IllegalThreadStateException. |
| 3 | volatile gives visibility, not atomicity; x++ is still a race. |
| 4 | sleep() keeps locks; wait() releases the lock. |
| 5 | wait/notify must be inside synchronized or → IllegalMonitorStateException. |
| 6 | setDaemon() must be called before start(). |
| 7 | Always unlock() in a finally block. |
| 8 | Callable returns a value and can throw checked exceptions; Runnable cannot. |
| 9 | Future.get() blocks the caller. |
| 10 | ConcurrentHashMap forbids null keys/values. |
| 11 | Forgetting shutdown() can keep the JVM alive. |
| 12 | thenApply maps to a value; thenCompose flattens a nested future. |
| 13 | Virtual threads should not be pooled — one per task. |
| 14 | synchronized pins a virtual thread; prefer ReentrantLock. |
| 15 | Deadlock needs all four Coffman conditions; fix via lock ordering. |
18. Common Mistakes
| Mistake | Fix |
|---|---|
Calling run() instead of start() | Use start() for concurrency. |
Using volatile for counters | Use AtomicInteger or synchronized. |
Forgetting to unlock() | Always unlock in finally. |
| Not shutting down an executor | Call shutdown() (or use try-with-resources). |
| Inconsistent lock ordering | Acquire locks in a global order. |
| Pooling virtual threads | Create one virtual thread per task. |
synchronized on virtual threads | Use ReentrantLock to avoid pinning. |
| Sharing non-thread-safe collections | Use concurrent collections. |
19. Interview Questions
Q1. Difference between a process and a thread? A process is an independent program with its own memory; a thread is a lightweight execution unit inside a process sharing its memory.
Q2. Difference between start() and run()?
start() creates a new thread and invokes run() on it; calling run() directly executes in the current thread with no new thread.
Q3. What is a race condition? A bug where concurrent threads access shared mutable state and the outcome depends on timing, causing inconsistent results.
Q4. What does synchronized do?
It allows only one thread at a time into a critical section by acquiring an object's monitor lock, providing mutual exclusion and visibility.
Q5. Difference between volatile and synchronized?
volatile guarantees visibility of a single variable; synchronized guarantees both mutual exclusion (atomicity) and visibility for a block.
Q6. What is a deadlock and how do you prevent it?
Two+ threads each waiting on locks held by the other. Prevent it with consistent lock ordering, tryLock with timeouts, or reducing lock scope.
Q7. Difference between Runnable and Callable?
Callable returns a value and can throw checked exceptions; Runnable returns nothing and cannot throw checked exceptions.
Q8. What is the difference between Future and CompletableFuture?
Future only lets you block and get a result; CompletableFuture supports non-blocking chaining, combining, and error handling.
Q9. Why use atomic classes? For lock-free, thread-safe updates to single variables using CPU compare-and-swap, which is faster than locking for simple operations.
Q10. What are virtual threads? Lightweight JVM-managed threads (Java 21) that unmount from carrier threads when blocked, enabling millions of concurrent tasks cheaply.
Q11. Difference between platform and virtual threads? Platform threads map 1:1 to costly OS threads; virtual threads are cheap, JVM-multiplexed onto a few carriers, ideal for I/O-bound concurrency.
Q12. What is thread pinning?
When a virtual thread cannot unmount from its carrier (e.g., inside synchronized or native code), reducing scalability — prefer ReentrantLock.
20. Quick Revision Notes
- Thread = unit of execution;
start()spawns,run()doesn't. - Lifecycle: NEW → RUNNABLE → (BLOCKED/WAITING/TIMED_WAITING) → TERMINATED.
start()twice →IllegalThreadStateException;setDaemonbeforestart.- Race condition: unsynchronized shared mutable state;
count++not atomic. synchronized= mutual exclusion + visibility;volatile= visibility only.wait/notifyinsidesynchronized;sleepkeeps locks,waitreleases.ReentrantLock: manual lock/unlock (finally!),tryLock, fairness, conditions.- Deadlock = 4 Coffman conditions; prevent with lock ordering.
- Executors: fixed/cached/single/scheduled/virtual-per-task; always
shutdown(). Runnable(void, no checked) vsCallable(value + checked).Future.get()blocks;CompletableFuturechains (thenApply/thenCompose/thenCombine/exceptionally).- Concurrent collections:
ConcurrentHashMap(no nulls),CopyOnWriteArrayList,BlockingQueue. - Atomics: lock-free single-variable ops via CAS.
- Virtual threads (21): millions cheap;
Thread.ofVirtual(),newVirtualThreadPerTaskExecutor(); don't pool. synchronizedpins virtual threads → preferReentrantLock.
21. One-Page Cheat Sheet
==================== MULTITHREADING & VIRTUAL THREADS CHEAT SHEET ====================
CREATE THREADS
extend Thread / implement Runnable (preferred) / lambda
start() = NEW thread ; run() = current thread ; start() twice -> ITSException
LIFECYCLE
NEW -start()-> RUNNABLE <-> RUNNING -> TERMINATED
BLOCKED (lock) / WAITING (wait,join) / TIMED_WAITING (sleep,wait(ms))
METHODS
sleep(ms) keeps locks | wait() releases lock | join() wait for finish
setDaemon(true) BEFORE start ; interrupt() request stop
RACE CONDITION
count++ = read-modify-write (not atomic) -> lost updates
fix: synchronized / Lock / Atomic
SYNCHRONIZATION
synchronized method/block -> monitor lock (1 thread)
static synchronized -> locks Class object
volatile = VISIBILITY only (not atomic)
wait/notify/notifyAll inside synchronized (else IllegalMonitorState)
LOCKS
ReentrantLock: lock(); try{...} finally{ unlock(); }
tryLock(), lockInterruptibly(), fairness, newCondition()
ReadWriteLock: many readers OR one writer
DEADLOCK (4 conditions: mutual excl, hold&wait, no preempt, circular wait)
prevent: consistent LOCK ORDERING / tryLock timeout
EXECUTOR FRAMEWORK
Executors.newFixedThreadPool(n)/Cached/SingleThread/ScheduledThreadPool
submit(Runnable/Callable) -> Future ; invokeAll(tasks)
shutdown() / shutdownNow() / awaitTermination()
ScheduledExecutorService: schedule / scheduleAtFixedRate
RUNNABLE vs CALLABLE
Runnable: run(), void, no checked ex
Callable<V>: call(), returns V, throws checked
FUTURE / COMPLETABLEFUTURE
Future: get()[blocks], isDone, cancel
CompletableFuture.supplyAsync(...).thenApply().thenCompose()
.thenCombine(other,fn).exceptionally(fn).join()
CONCURRENT COLLECTIONS
ConcurrentHashMap (NO null keys/values), CopyOnWriteArrayList
BlockingQueue (put/take block), ConcurrentLinkedQueue
ATOMIC (lock-free, CAS)
AtomicInteger/Long/Boolean/Reference
incrementAndGet, addAndGet, compareAndSet
VIRTUAL THREADS (Java 21)
Thread.ofVirtual().start(r) ; Executors.newVirtualThreadPerTaskExecutor()
millions cheap ; unmount on block ; DON'T pool ; isVirtual()
PINNING: synchronized/native pins carrier -> use ReentrantLock
PLATFORM vs VIRTUAL
platform: OS, 1:1, ~1MB, thousands, CPU-bound, pool
virtual: JVM, many:few, tiny, millions, I/O-bound, no pool
=====================================================================================
End of 10 - Multithreading and Virtual Threads.