1Z0-830 Concurrency - Java SE 21 Certification Prep
Creating Threads
Java provides multiple ways to create and start threads. Understanding these approaches and their differences is essential for the 1Z0-830 exam.
// Method 1: Extend Thread class
// Less flexible - cannot extend another class
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Running in: " + getName());
}
}
MyThread thread = new MyThread();
thread.start(); // Creates new thread and calls run()
// thread.run(); // WRONG! Calls run() in current thread, no new thread created
// Method 2: Implement Runnable interface (preferred)
// More flexible - can extend other classes, better separation of concerns
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Running in: " + Thread.currentThread().getName());
}
}
Thread thread = new Thread(new MyRunnable());
thread.start();
// Method 3: Lambda expression (most concise for simple tasks)
Thread thread = new Thread(() -> {
System.out.println("Running in lambda");
});
thread.start();
// Method 4: Method reference
Thread thread = new Thread(MyClass::myStaticMethod);
thread.start();
// Thread with custom name
Thread named = new Thread(() -> {
// Task code
}, "worker-1");
System.out.println(named.getName()); // Prints: worker-1
// Daemon threads - background threads that don't prevent JVM shutdown
Thread daemon = new Thread(() -> {
while (true) {
// Background work
}
});
daemon.setDaemon(true); // Must be called before start()
daemon.start();
// JVM exits when only daemon threads remain running
// Common mistake - setting daemon after start
Thread t = new Thread(() -> {});
t.start();
t.setDaemon(true); // Throws IllegalThreadStateException!
// Thread priority (usually leave at default)
thread.setPriority(Thread.MIN_PRIORITY); // 1
thread.setPriority(Thread.NORM_PRIORITY); // 5 (default)
thread.setPriority(Thread.MAX_PRIORITY); // 10
// Note: Priority is only a hint to the scheduler
Exam Tip: Remember that calling run() directly does not create a new thread - you must call start(). Calling start() twice on the same thread throws IllegalThreadStateException. Daemon status must be set before calling start().
Thread Lifecycle
Understanding thread states is crucial for debugging concurrent applications and for the certification exam.
Thread States (Thread.State enum):
- NEW - Thread created but start() not yet called
- RUNNABLE - Thread executing or ready to execute (includes running and ready states)
- BLOCKED - Thread waiting to acquire a monitor lock for synchronized block/method
- WAITING - Thread waiting indefinitely for another thread (wait(), join())
- TIMED_WAITING - Thread waiting for specified time (sleep(), wait(timeout), join(timeout))
- TERMINATED - Thread completed execution or terminated due to exception
Thread thread = new Thread(() -> {
try {
System.out.println("Task executing");
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// Check thread state
Thread.State state = thread.getState(); // Returns NEW
thread.start();
state = thread.getState(); // Returns RUNNABLE
// Thread control methods
// Sleep - pauses current thread for specified time
// Thread enters TIMED_WAITING state
// Does NOT release any locks held
Thread.sleep(1000); // Sleep for 1 second
Thread.sleep(Duration.ofSeconds(1)); // Java 19+ - using Duration
// Join - wait for another thread to complete
// Current thread enters WAITING or TIMED_WAITING state
thread.join(); // Wait indefinitely for thread to complete
thread.join(1000); // Wait up to 1 second
thread.join(Duration.ofSeconds(1)); // Java 19+ - using Duration
// Yield - hint to scheduler that current thread is willing to yield
// Thread stays in RUNNABLE state
// Rarely needed in practice
Thread.yield();
// Interrupting threads - cooperative mechanism
thread.interrupt(); // Sets interrupt flag on target thread
Thread.interrupted(); // Checks and clears interrupt flag of current thread (static)
thread.isInterrupted(); // Checks interrupt flag without clearing (instance method)
// Proper interruption handling
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// Sleep was interrupted - clean up and exit
System.out.println("Thread was interrupted during sleep");
Thread.currentThread().interrupt(); // Restore interrupt status
return; // Exit gracefully
}
// Checking for interruption in long-running task
while (!Thread.currentThread().isInterrupted()) {
// Do work
processNextItem();
}
// Thread information methods
String name = thread.getName(); // Get thread name
long id = thread.threadId(); // Get thread ID (Java 19+)
boolean alive = thread.isAlive(); // Check if thread is alive
boolean daemon = thread.isDaemon(); // Check if daemon
int priority = thread.getPriority(); // Get priority
Thread.State state = thread.getState(); // Get current state
// Cannot restart a terminated thread
Thread t = new Thread(() -> System.out.println("Task"));
t.start();
t.join(); // Wait for completion
t.start(); // Throws IllegalThreadStateException!
Exam Tip: sleep() throws InterruptedException and must be handled. sleep() does not release locks. Thread.interrupted() is static and clears the flag, while isInterrupted() is an instance method and does not clear it. This distinction is frequently tested.
Synchronization
Synchronization prevents race conditions by ensuring that only one thread can access a critical section at a time. Understanding different synchronization techniques is essential.
// Problem: Race condition without synchronization
public class Counter {
private int count = 0;
// NOT thread-safe! count++ is three operations: read, increment, write
public void increment() {
count++; // Multiple threads can interfere with each other
}
public int getCount() {
return count;
}
}
// Solution 1: Synchronized instance method
// Locks on the instance (this)
public class Counter {
private int count = 0;
public synchronized void increment() {
count++; // Only one thread can execute this at a time
}
public synchronized int getCount() {
return count;
}
}
// Solution 2: Synchronized block for finer control
// Lock only the critical section, not entire method
public void process() {
// Non-synchronized code - can run concurrently
doPreparation();
synchronized (this) {
// Critical section - only one thread at a time
count++;
updateState();
}
// Non-synchronized code - can run concurrently
doCleanup();
}
// Solution 3: Synchronized on separate lock object (preferred)
// More flexibility and control
private final Object lock = new Object(); // Dedicated lock object
public void process() {
synchronized (lock) {
// Critical section protected by separate lock
count++;
}
}
// Static synchronized method - locks on the Class object
public class Counter {
private static int globalCount = 0;
// Locks on Counter.class, not on instance
public static synchronized void incrementGlobal() {
globalCount++;
}
}
// Equivalent synchronized block for static method
public static void incrementGlobal() {
synchronized (Counter.class) {
globalCount++;
}
}
// wait/notify/notifyAll - thread coordination mechanism
// Must be called from synchronized context on the same object
// Producer thread
synchronized (lock) {
while (queue.isFull()) {
lock.wait(); // Releases lock and waits to be notified
}
queue.add(item);
lock.notifyAll(); // Wake up all waiting threads
}
// Consumer thread
synchronized (lock) {
while (queue.isEmpty()) {
lock.wait(); // Releases lock and waits to be notified
}
Item item = queue.remove();
lock.notifyAll(); // Wake up all waiting threads
}
// wait with timeout
synchronized (lock) {
while (!condition) {
lock.wait(1000); // Wait up to 1 second
// Check timeout condition
}
}
// Common mistakes
// Mistake 1: Calling wait/notify outside synchronized block
lock.wait(); // Throws IllegalMonitorStateException!
// Mistake 2: Using if instead of while for wait
synchronized (lock) {
if (!condition) { // WRONG! Use while loop
lock.wait();
}
}
// Correct: Always use while loop
synchronized (lock) {
while (!condition) { // Correct - handles spurious wakeups
lock.wait();
}
}
// Mistake 3: Synchronizing on different objects
synchronized (lock1) {
lock2.wait(); // Throws IllegalMonitorStateException!
}
// Reentrant locks - thread can acquire same lock multiple times
public synchronized void outer() {
System.out.println("In outer");
inner(); // Can call synchronized method from synchronized method
}
public synchronized void inner() {
System.out.println("In inner"); // Same thread, same lock - OK
}
Exam Tip: wait(), notify(), and notifyAll() must be called from a synchronized context on the same object, otherwise IllegalMonitorStateException is thrown. Always use while loop with wait() to handle spurious wakeups. notify() wakes one thread, notifyAll() wakes all waiting threads. When in doubt, use notifyAll().
ExecutorService
The Executor framework provides a higher-level replacement for working with threads directly. It manages thread pools and simplifies task submission and lifecycle management.
// Creating different types of executors
// Single thread executor - tasks execute sequentially
// Good for: tasks that must execute in order
ExecutorService single = Executors.newSingleThreadExecutor();
// Fixed thread pool - fixed number of threads
// Good for: limiting resource usage, CPU-bound tasks
ExecutorService fixed = Executors.newFixedThreadPool(4);
// Cached thread pool - creates threads as needed, reuses idle threads
// Good for: many short-lived tasks
ExecutorService cached = Executors.newCachedThreadPool();
// Scheduled executor - supports delayed and periodic tasks
// Good for: scheduled tasks, recurring jobs
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
// Virtual thread executor (Java 21+) - lightweight threads
// Good for: I/O-bound tasks, high concurrency
ExecutorService virtual = Executors.newVirtualThreadPerTaskExecutor();
// Submitting tasks
// execute() - fire and forget, no return value
// Takes Runnable only
executor.execute(() -> System.out.println("Task executing"));
// submit() with Runnable - returns Future> for tracking
Future> future = executor.submit(() -> {
System.out.println("Task executing");
// No return value
});
// submit() with Callable - returns Future with result
// Callable can return a value and throw checked exceptions
Future result = executor.submit(() -> {
Thread.sleep(1000);
return "Task completed";
});
// Getting results from Future
try {
// Blocking wait for result
String value = result.get(); // Blocks until task completes
System.out.println(value);
// Wait with timeout
String timedValue = result.get(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
// Thread was interrupted while waiting
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
// Task threw an exception
Throwable cause = e.getCause();
System.err.println("Task failed: " + cause);
} catch (TimeoutException e) {
// Timeout expired before task completed
System.err.println("Task timed out");
}
// Future methods for task management
boolean done = result.isDone(); // Check if task completed
boolean cancelled = result.isCancelled(); // Check if task was cancelled
// Cancel task
// mayInterruptIfRunning=true interrupts thread if task is running
// mayInterruptIfRunning=false cancels only if task hasn't started
boolean wasCancelled = result.cancel(true);
if (result.isCancelled()) {
// Task was cancelled, get() will throw CancellationException
}
// Executor shutdown - MUST shut down to release resources
// Graceful shutdown - no new tasks accepted, existing tasks complete
executor.shutdown();
System.out.println("Shutdown initiated");
// Check if shutdown
boolean isShutdown = executor.isShutdown();
// Wait for tasks to complete
try {
// Block up to 60 seconds for termination
boolean terminated = executor.awaitTermination(60, TimeUnit.SECONDS);
if (!terminated) {
System.err.println("Tasks did not finish in time");
executor.shutdownNow(); // Force shutdown
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
// Force shutdown - attempts to stop all executing tasks
List notExecuted = executor.shutdownNow();
System.out.println("Tasks that never started: " + notExecuted.size());
// Check if all tasks completed
boolean isTerminated = executor.isTerminated();
// try-with-resources (Java 19+) - automatically shuts down
try (var executor = Executors.newFixedThreadPool(4)) {
executor.submit(() -> doWork());
// Executor automatically shuts down when leaving try block
}
// Submitting multiple tasks
List<Callable<String>> tasks = List.of(
() -> "Result 1",
() -> "Result 2",
() -> "Result 3"
);
// invokeAll - submit all tasks and wait for all to complete
// Returns list of Futures in same order as input
List<Future<String>> futures = executor.invokeAll(tasks);
for (Future<String> future : futures) {
String result = future.get(); // All tasks already complete
System.out.println(result);
}
// invokeAll with timeout
List<Future<String>> futures = executor.invokeAll(tasks, 10, TimeUnit.SECONDS);
// invokeAny - returns result of first successful task
// Cancels remaining tasks once one completes
String firstResult = executor.invokeAny(tasks);
System.out.println("First result: " + firstResult);
// invokeAny with timeout
String firstResult = executor.invokeAny(tasks, 5, TimeUnit.SECONDS);
// Scheduled executor tasks
// Execute once after delay
scheduled.schedule(() -> {
System.out.println("Executed after 5 seconds");
}, 5, TimeUnit.SECONDS);
// Execute periodically at fixed rate
// If task takes longer than period, next execution waits
// Good for: tasks that should run at consistent intervals
ScheduledFuture> fixedRate = scheduled.scheduleAtFixedRate(
() -> System.out.println("Running every second"),
0, // Initial delay
1, // Period
TimeUnit.SECONDS
);
// Execute with fixed delay between executions
// Delay measured from completion of one execution to start of next
// Good for: tasks where you want guaranteed rest time between executions
ScheduledFuture> fixedDelay = scheduled.scheduleWithFixedDelay(
() -> System.out.println("Running with 1 second between completions"),
0, // Initial delay
1, // Delay between executions
TimeUnit.SECONDS
);
// Cancel scheduled task
fixedRate.cancel(true);
Exam Tip: ExecutorService must be shut down explicitly or tasks may prevent JVM from exiting. shutdown() is graceful (waits for tasks), shutdownNow() is forceful (attempts to stop tasks). invokeAll() waits for all tasks to complete, invokeAny() returns when first task succeeds. Remember to handle InterruptedException, ExecutionException, and TimeoutException when using Future.get().
Concurrent Collections
Java provides thread-safe collection implementations that avoid the need for external synchronization. Understanding their characteristics and use cases is important for writing efficient concurrent code.// Thread-safe collection classes // ConcurrentHashMap - thread-safe HashMap alternative // Better performance than Collections.synchronizedMap() // Does NOT allow null keys or null values ConcurrentHashMapmap = new ConcurrentHashMap<>(); // ConcurrentLinkedQueue - unbounded thread-safe queue // Lock-free using CAS (Compare-And-Swap) ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); // CopyOnWriteArrayList - thread-safe ArrayList // Best for read-heavy scenarios (many reads, few writes) // Creates copy of array on every modification CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); // CopyOnWriteArraySet - thread-safe Set backed by CopyOnWriteArrayList CopyOnWriteArraySet set = new CopyOnWriteArraySet<>(); // ConcurrentSkipListMap - thread-safe sorted map // Alternative to TreeMap for concurrent use ConcurrentSkipListMap sortedMap = new ConcurrentSkipListMap<>(); // ConcurrentSkipListSet - thread-safe sorted set ConcurrentSkipListSet sortedSet = new ConcurrentSkipListSet<>(); // ConcurrentHashMap operations map.put("key1", 1); // Standard put map.putIfAbsent("key1", 2); // Put only if absent, returns existing or null map.replace("key1", 1, 10); // Replace if current value matches // Atomic compute operations - eliminate race conditions map.computeIfAbsent("key2", k -> { // Expensive computation only if key is absent return expensiveComputation(k); }); map.computeIfPresent("key1", (k, v) -> { // Compute new value based on existing value return v + 1; }); map.compute("key1", (k, v) -> { // Compute value whether key exists or not return (v == null) ? 1 : v + 1; }); // merge - combines old value with new value map.merge("key1", 1, Integer::sum); // Adds 1 to existing value map.merge("key1", 1, (oldVal, newVal) -> oldVal + newVal); // Bulk operations map.forEach((k, v) -> System.out.println(k + "=" + v)); map.replaceAll((k, v) -> v * 2); // Double all values // Search operations return null if not found String result = map.search(1, (k, v) -> { return v > 100 ? k : null; }); // Reduce operations int sum = map.reduce(1, (k, v) -> v, Integer::sum); // Performance note: Do NOT use size() in conditional logic if (map.size() == 0) { // Potentially expensive operation // Do something } // Better: use isEmpty() if (map.isEmpty()) { // More efficient // Do something } // Blocking queues - queues that wait when full or empty // LinkedBlockingQueue - optionally bounded queue backed by linked nodes BlockingQueue linkedQueue = new LinkedBlockingQueue<>(); BlockingQueue boundedQueue = new LinkedBlockingQueue<>(100); // Capacity 100 // ArrayBlockingQueue - bounded queue backed by array // Must specify capacity at creation BlockingQueue arrayQueue = new ArrayBlockingQueue<>(100); // PriorityBlockingQueue - unbounded blocking priority queue BlockingQueue priorityQueue = new PriorityBlockingQueue<>(); // SynchronousQueue - no storage capacity, direct handoff // Each put must wait for a take and vice versa BlockingQueue synchronousQueue = new SynchronousQueue<>(); // DelayQueue - elements can only be taken when delay has expired BlockingQueue delayQueue = new DelayQueue<>(); // Blocking queue operations // put() - blocks if queue is full (for bounded queues) blockingQueue.put("item"); // take() - blocks if queue is empty String item = blockingQueue.take(); // offer() with timeout - waits up to specified time boolean added = blockingQueue.offer("item", 1, TimeUnit.SECONDS); if (added) { System.out.println("Item added"); } else { System.out.println("Queue remained full for 1 second"); } // poll() with timeout - waits up to specified time String item = blockingQueue.poll(1, TimeUnit.SECONDS); if (item != null) { System.out.println("Got item: " + item); } else { System.out.println("Queue remained empty for 1 second"); } // Non-blocking operations boolean added = blockingQueue.offer("item"); // Returns false if full String item = blockingQueue.poll(); // Returns null if empty // CopyOnWriteArrayList characteristics CopyOnWriteArrayList cowList = new CopyOnWriteArrayList<>(); // Writes are expensive - creates copy of entire array cowList.add("item1"); // Copies entire array cowList.add("item2"); // Copies entire array again cowList.remove("item1"); // Copies entire array again // Reads are fast - no locking needed String item = cowList.get(0); // Fast, no synchronization // Iteration never throws ConcurrentModificationException // Iterator sees snapshot of list at time iterator was created for (String item : cowList) { // Other threads can modify cowList during iteration // This iteration sees consistent snapshot System.out.println(item); } // Best use case: read-heavy workloads // Example: event listener lists private final CopyOnWriteArrayList listeners = new CopyOnWriteArrayList<>(); public void addListener(EventListener listener) { listeners.add(listener); // Rare operation } public void fireEvent(Event event) { for (EventListener listener : listeners) { // Frequent operation listener.onEvent(event); } } // Comparing thread-safe collections // Collections.synchronizedMap vs ConcurrentHashMap Map syncMap = Collections.synchronizedMap(new HashMap<>()); // synchronizedMap: Locks entire map for each operation, allows null // ConcurrentHashMap: Fine-grained locking, better performance, no null // ArrayList vs CopyOnWriteArrayList // ArrayList: Not thread-safe, fast modifications // CopyOnWriteArrayList: Thread-safe, slow modifications, fast reads // LinkedList vs ConcurrentLinkedQueue // LinkedList: Not thread-safe // ConcurrentLinkedQueue: Thread-safe, lock-free
Exam Tip: ConcurrentHashMap does NOT allow null keys or values (throws NullPointerException). CopyOnWriteArrayList is best for read-heavy scenarios because writes copy the entire array. BlockingQueue methods: put()/take() block indefinitely, offer()/poll() with timeout block for specified time, offer()/poll() without timeout never block. Remember that size() on ConcurrentHashMap can be expensive and might not reflect exact size during concurrent modifications.
Atomic Classes
Atomic classes provide lock-free thread-safe operations on single variables. They use low-level atomic machine instructions for better performance than synchronized blocks.// Atomic variable classes - in java.util.concurrent.atomic package AtomicInteger counter = new AtomicInteger(0); AtomicLong longCounter = new AtomicLong(0); AtomicBoolean flag = new AtomicBoolean(false); AtomicReferenceref = new AtomicReference<>("initial"); // AtomicInteger common operations int current = counter.get(); // Read current value counter.set(10); // Set value // Atomic increment and decrement int oldValue = counter.getAndIncrement(); // Returns old value, then increments (i++) int newValue = counter.incrementAndGet(); // Increments, then returns new value (++i) int oldValue = counter.getAndDecrement(); // Returns old value, then decrements (i--) int newValue = counter.decrementAndGet(); // Decrements, then returns new value (--i) // Atomic add int oldValue = counter.getAndAdd(5); // Returns old value, then adds 5 int newValue = counter.addAndGet(5); // Adds 5, then returns new value // Compare-and-set (CAS) - foundation of lock-free algorithms boolean success = counter.compareAndSet(10, 20); // If current value is 10, set to 20 and return true // If current value is not 10, don't change and return false // Example: Increment only if value is less than 100 int current; do { current = counter.get(); if (current >= 100) break; } while (!counter.compareAndSet(current, current + 1)); // Atomic update with function (Java 8+) counter.updateAndGet(x -> x * 2); // Double the value, return new value counter.getAndUpdate(x -> x * 2); // Double the value, return old value // Accumulate with binary operator counter.accumulateAndGet(5, (current, delta) -> current + delta); counter.getAndAccumulate(5, Integer::sum); // AtomicLong - same operations as AtomicInteger but for long values AtomicLong longCounter = new AtomicLong(0); longCounter.incrementAndGet(); longCounter.addAndGet(1000000L); // AtomicBoolean - atomic boolean operations AtomicBoolean flag = new AtomicBoolean(false); boolean oldValue = flag.getAndSet(true); // Set to true, return old value boolean success = flag.compareAndSet(false, true); // Set if currently false // Common pattern: execute once if (flag.compareAndSet(false, true)) { // This block executes only once across all threads performExpensiveInitialization(); } // AtomicReference - atomic operations on object references AtomicReference ref = new AtomicReference<>("initial"); String oldValue = ref.get(); ref.set("new value"); String oldValue = ref.getAndSet("new value"); boolean success = ref.compareAndSet("expected", "new"); // Update with function ref.updateAndGet(s -> s.toUpperCase()); ref.getAndUpdate(s -> s.toUpperCase()); // Example: Thread-safe lazy initialization private final AtomicReference cache = new AtomicReference<>(); public ExpensiveObject getInstance() { ExpensiveObject obj = cache.get(); if (obj == null) { obj = new ExpensiveObject(); if (!cache.compareAndSet(null, obj)) { // Another thread already initialized it obj = cache.get(); } } return obj; } // Array atomic classes // AtomicIntegerArray - array of atomic integers AtomicIntegerArray array = new AtomicIntegerArray(10); array.set(0, 100); array.getAndIncrement(0); // Increment element at index 0 array.compareAndSet(0, 100, 200); // AtomicLongArray and AtomicReferenceArray work similarly // LongAdder and DoubleAdder - optimized for high contention // Better performance than AtomicLong for frequent updates LongAdder adder = new LongAdder(); // Multiple threads can call these concurrently adder.increment(); // Equivalent to add(1) adder.decrement(); // Equivalent to add(-1) adder.add(5); // Get sum - may not reflect very recent updates in high contention long total = adder.sum(); // Reset to zero adder.reset(); // LongAdder vs AtomicLong // AtomicLong: Single variable, good for low to medium contention // LongAdder: Multiple variables that sum to total, better for high contention // Trade-off: LongAdder uses more memory but has better throughput // Example: High-contention counter public class MetricsCollector { private final LongAdder requestCount = new LongAdder(); public void recordRequest() { requestCount.increment(); // Very fast even with many threads } public long getTotalRequests() { return requestCount.sum(); } } // DoubleAdder - like LongAdder but for double values DoubleAdder doubleAdder = new DoubleAdder(); doubleAdder.add(3.14); double sum = doubleAdder.sum(); // LongAccumulator and DoubleAccumulator - generalized version // Allows custom accumulation function LongAccumulator max = new LongAccumulator(Long::max, Long.MIN_VALUE); max.accumulate(100); max.accumulate(200); max.accumulate(150); long maxValue = max.get(); // Returns 200 // Volatile vs Atomic classes // volatile: Ensures visibility but NOT atomicity private volatile int count; // Writes are visible to all threads immediately count++; // Still NOT atomic! (read, increment, write are separate operations) // AtomicInteger: Ensures both visibility AND atomicity private AtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); // Atomic operation, thread-safe // When to use what: // - volatile: Simple reads/writes, visibility only (e.g., flags) // - Atomic classes: When you need atomic read-modify-write operations // - synchronized: When you need to protect multiple operations as a unit // Example showing the difference private volatile int nonAtomicCounter = 0; private AtomicInteger atomicCounter = new AtomicInteger(0); // NOT thread-safe even with volatile nonAtomicCounter++; // Three operations: read, increment, write // Thread-safe with atomic atomicCounter.incrementAndGet(); // Single atomic operation
Exam Tip: Atomic classes use CAS (Compare-And-Set) operations that are lock-free and more efficient than synchronized blocks for single-variable updates. Remember that volatile provides visibility guarantees but not atomicity - volatile int++ is still a race condition. LongAdder provides better performance than AtomicLong under high contention at the cost of higher memory usage. The compare-and-set pattern is fundamental to lock-free programming.
Virtual Threads (Java 21)
Virtual threads are lightweight threads managed by the JVM rather than the operating system. They enable writing high-throughput concurrent applications using simple thread-per-task style.
// Creating virtual threads
// Basic virtual thread creation
Thread vThread = Thread.ofVirtual().start(() -> {
System.out.println("Running in virtual thread!");
});
// Virtual threads are daemon by default
System.out.println(vThread.isDaemon()); // true
// Named virtual thread
Thread named = Thread.ofVirtual()
.name("worker-1")
.start(() -> {
System.out.println("Named virtual thread: " + Thread.currentThread().getName());
});
// Unstarted virtual thread
Thread unstarted = Thread.ofVirtual()
.unstarted(() -> {
System.out.println("This virtual thread is not started yet");
});
unstarted.start(); // Start it later
// Virtual thread factory - for creating multiple threads
ThreadFactory factory = Thread.ofVirtual()
.name("worker-", 0) // Names will be worker-0, worker-1, worker-2, etc.
.factory();
Thread t1 = factory.newThread(() -> task());
Thread t2 = factory.newThread(() -> task());
t1.start();
t2.start();
// Virtual thread executor (recommended approach)
// Creates new virtual thread for each submitted task
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// Submit I/O-bound tasks
executor.submit(() -> {
// Blocking I/O operation - virtual thread yields automatically
String response = httpClient.send(request);
return response;
});
executor.submit(() -> {
// Another blocking operation
ResultSet rs = statement.executeQuery(sql);
return processResults(rs);
});
} // Executor automatically shuts down when leaving try block
// Can handle massive numbers of concurrent tasks
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 1_000_000).forEach(i -> {
executor.submit(() -> {
try {
Thread.sleep(Duration.ofSeconds(1));
return "Task " + i + " completed";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
});
});
}
// 1 million concurrent tasks! Not possible with platform threads
// Checking if thread is virtual
Thread current = Thread.currentThread();
if (current.isVirtual()) {
System.out.println("Running in virtual thread");
} else {
System.out.println("Running in platform thread");
}
// Platform threads vs Virtual threads comparison
// Platform thread (traditional):
// - 1:1 mapping to OS thread
// - Each thread uses approximately 1-2 MB of stack memory
// - Limited by OS thread limits (typically thousands)
// - Context switching is expensive
// - Good for: CPU-bound tasks
// Virtual thread:
// - Many:1 mapping to carrier (platform) threads
// - Each thread uses only a few KB of stack memory
// - Can have millions of virtual threads
// - Automatically yields on blocking operations
// - Good for: I/O-bound tasks (network, file, database)
// Creating platform thread explicitly (Java 21+)
Thread platformThread = Thread.ofPlatform().start(() -> {
// CPU-intensive computation
calculatePrimes();
});
// When virtual threads yield (unmount from carrier thread):
// - Blocking I/O operations
// - Thread.sleep()
// - Blocking synchronization (Object.wait(), Lock operations)
// Virtual threads do NOT yield on:
// - synchronized blocks (pins carrier thread)
// - CPU-intensive computations
// Working with existing APIs - virtual threads are compatible
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// invokeAll works with virtual threads
List<Callable<String>> tasks = List.of(
() -> fetchFromDatabase(),
() -> callExternalAPI(),
() -> readFromFile()
);
List<Future<String>> futures = executor.invokeAll(tasks);
// All tasks run concurrently in virtual threads
// invokeAny also works
String firstResult = executor.invokeAny(tasks);
}
// Structured concurrency (preview feature)
// Groups related tasks with shared lifecycle
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future user = scope.fork(() -> fetchUser());
Future order = scope.fork(() -> fetchOrder());
scope.join(); // Wait for all tasks
scope.throwIfFailed(); // Throw if any failed
// Both succeeded, use results
processData(user.resultNow(), order.resultNow());
}
// Best practices for virtual threads
// DO: Use for I/O-bound tasks
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
// Perfect use case: I/O operation
String content = Files.readString(Path.of("file.txt"));
return content;
});
}
// DON'T: Use synchronized for long operations (pins carrier thread)
// Bad:
public synchronized void processRequest() {
callSlowExternalAPI(); // Blocks carrier thread!
}
// Good: Use ReentrantLock instead
private final ReentrantLock lock = new ReentrantLock();
public void processRequest() {
lock.lock();
try {
callSlowExternalAPI(); // Virtual thread can yield
} finally {
lock.unlock();
}
}
// DON'T: Use ThreadLocal excessively (memory overhead)
// Each virtual thread gets its own copy - millions of threads = memory issues
private static final ThreadLocal threadLocal = new ThreadLocal<>();
// Good: Use scoped values (preview feature) instead
private static final ScopedValue scopedValue = ScopedValue.newInstance();
// DON'T: Use for CPU-bound tasks
// Bad: CPU-intensive work in virtual thread
executor.submit(() -> {
// Complex computation that doesn't block
for (long i = 0; i < 1_000_000_000; i++) {
compute(i); // Just uses CPU, never yields
}
});
// Good: Use platform threads for CPU-bound work
ExecutorService cpuExecutor = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
cpuExecutor.submit(() -> {
// CPU-intensive work
computeIntensiveTask();
});
// Monitoring virtual threads
Thread vThread = Thread.ofVirtual().start(() -> {});
Thread.State state = vThread.getState(); // Works like platform threads
long threadId = vThread.threadId(); // Unique ID
String name = vThread.getName(); // Thread name
// Virtual thread pools are NOT needed
// Don't do this:
// ExecutorService pool = Executors.newFixedThreadPool(1000); // Virtual threads
// Instead, create thread per task:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// Each task gets its own virtual thread
}
Exam Tip: Virtual threads are designed for I/O-bound tasks where threads spend most time waiting (network calls, database queries, file I/O). They automatically yield during blocking operations. For CPU-bound tasks, use platform threads. Avoid using synchronized blocks for long operations with virtual threads - use ReentrantLock instead. Virtual threads are daemon threads by default and are very lightweight (thousands of bytes vs megabytes for platform threads).
Locks
The java.util.concurrent.locks package provides more flexible locking mechanisms than synchronized blocks. These explicit locks offer additional capabilities like try-lock, interruptible locks, and separate read/write locks.
// ReentrantLock - more flexible alternative to synchronized
ReentrantLock lock = new ReentrantLock();
// Basic usage - must unlock in finally block
lock.lock();
try {
// Critical section - only one thread can execute this
modifySharedData();
} finally {
lock.unlock(); // ALWAYS unlock in finally to prevent deadlock
}
// Lock is reentrant - same thread can acquire it multiple times
public void outer() {
lock.lock();
try {
inner(); // Same thread can acquire lock again
} finally {
lock.unlock();
}
}
public void inner() {
lock.lock();
try {
// Critical section
} finally {
lock.unlock();
}
}
// tryLock - non-blocking attempt to acquire lock
if (lock.tryLock()) {
try {
// Got the lock, do work
criticalSection();
} finally {
lock.unlock();
}
} else {
// Could not get lock, do something else
handleBusyResource();
}
// tryLock with timeout - wait for specified time
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// Got the lock within 1 second
criticalSection();
} finally {
lock.unlock();
}
} else {
// Timeout - could not acquire lock in 1 second
handleTimeout();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// lockInterruptibly - can be interrupted while waiting
try {
lock.lockInterruptibly();
try {
// Critical section
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// Thread was interrupted while waiting for lock
Thread.currentThread().interrupt();
}
// Fair vs unfair locks
// Unfair lock (default) - better performance
ReentrantLock unfairLock = new ReentrantLock(false);
// Fair lock - threads acquire lock in order they requested it
// Prevents starvation but lower throughput
ReentrantLock fairLock = new ReentrantLock(true);
// Lock information methods
boolean isHeld = lock.isLocked(); // Is lock currently held?
boolean heldByMe = lock.isHeldByCurrentThread(); // Does current thread hold it?
int holdCount = lock.getHoldCount(); // Number of holds by current thread
boolean hasFairness = lock.isFair(); // Is this a fair lock?
int waiting = lock.getQueueLength(); // Approximate number of threads waiting
// Condition variables - more flexible than wait/notify
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// Producer
lock.lock();
try {
while (queue.isFull()) {
notFull.await(); // Wait until queue not full
}
queue.add(item);
notEmpty.signal(); // Wake up consumers
} finally {
lock.unlock();
}
// Consumer
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // Wait until queue not empty
}
Item item = queue.remove();
notFull.signal(); // Wake up producers
} finally {
lock.unlock();
}
// Condition methods
condition.await(); // Wait indefinitely
condition.await(1, TimeUnit.SECONDS); // Wait with timeout
condition.awaitUninterruptibly(); // Wait, can't be interrupted
condition.signal(); // Wake up one waiting thread
condition.signalAll(); // Wake up all waiting threads
// ReadWriteLock - allows multiple readers OR one writer
// Improves performance for read-heavy workloads
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
// Reading - multiple threads can hold read lock simultaneously
readLock.lock();
try {
// Multiple threads can read concurrently
return data;
} finally {
readLock.unlock();
}
// Writing - only one thread can hold write lock
// No readers or other writers allowed
writeLock.lock();
try {
// Exclusive access for writing
data = newValue;
} finally {
writeLock.unlock();
}
// Read lock cannot be upgraded to write lock
readLock.lock();
try {
if (needsUpdate()) {
// Must release read lock first
readLock.unlock();
writeLock.lock();
try {
// Now have write access
performUpdate();
} finally {
writeLock.unlock();
}
readLock.lock(); // Reacquire read lock
}
} finally {
if (readLock.tryLock()) { // Check if we still hold it
readLock.unlock();
}
}
// Write lock can be downgraded to read lock
writeLock.lock();
try {
performUpdate();
// Acquire read lock before releasing write lock
readLock.lock();
} finally {
writeLock.unlock();
}
try {
// Now holding read lock
continueReading();
} finally {
readLock.unlock();
}
// Fair ReadWriteLock - prevents starvation
ReadWriteLock fairRWLock = new ReentrantReadWriteLock(true);
// StampedLock - optimistic reading without locking (Java 8+)
// More complex but can offer better performance
StampedLock stampedLock = new StampedLock();
// Optimistic read - no actual lock acquired
long stamp = stampedLock.tryOptimisticRead();
// Read data
int value = data;
if (!stampedLock.validate(stamp)) {
// Data was modified during read, acquire actual read lock
stamp = stampedLock.readLock();
try {
value = data;
} finally {
stampedLock.unlockRead(stamp);
}
}
// Pessimistic read lock
long stamp = stampedLock.readLock();
try {
return data;
} finally {
stampedLock.unlockRead(stamp);
}
// Write lock
long stamp = stampedLock.writeLock();
try {
data = newValue;
} finally {
stampedLock.unlockWrite(stamp);
}
// Convert optimistic read to read lock
long stamp = stampedLock.tryOptimisticRead();
if (!stampedLock.validate(stamp)) {
stamp = stampedLock.readLock(); // Upgrade to read lock
}
// Comparing lock types
// synchronized:
// - Implicit lock acquisition and release
// - Cannot try to acquire lock without blocking
// - Cannot time out while waiting for lock
// - Cannot be interrupted while waiting for lock
// - Simple syntax
// ReentrantLock:
// - Explicit lock()/unlock() calls
// - Can try to acquire lock (tryLock)
// - Can time out while waiting
// - Can be interrupted while waiting
// - More flexible but more complex
// ReadWriteLock:
// - Multiple readers OR one writer
// - Good for read-heavy workloads
// - More overhead than simple locks
// StampedLock:
// - Optimistic reading without locking
// - Best performance for read-heavy scenarios
// - Most complex to use correctly
// - No reentrant support
// Common deadlock pattern to avoid
// Thread 1
lock1.lock();
try {
lock2.lock(); // Waits for lock2
try {
// Work
} finally {
lock2.unlock();
}
} finally {
lock1.unlock();
}
// Thread 2
lock2.lock();
try {
lock1.lock(); // Waits for lock1 - DEADLOCK!
try {
// Work
} finally {
lock1.unlock();
}
} finally {
lock2.unlock();
}
// Solution: Always acquire locks in same order
private void safeMethod() {
Lock firstLock = lock1.hashCode() < lock2.hashCode() ? lock1 : lock2;
Lock secondLock = firstLock == lock1 ? lock2 : lock1;
firstLock.lock();
try {
secondLock.lock();
try {
// Work - no deadlock possible
} finally {
secondLock.unlock();
}
} finally {
firstLock.unlock();
}
}
Exam Tip: Always unlock in a finally block to prevent leaving lock held if an exception occurs. ReentrantLock offers tryLock() for non-blocking lock attempts, lockInterruptibly() for interruptible lock acquisition, and timed tryLock(). ReadWriteLock allows multiple concurrent readers but only one writer. Read locks cannot be upgraded to write locks directly - must release read lock first. Write locks can be downgraded to read locks. StampedLock provides optimistic reads for best performance but is not reentrant.
1Z0-830 Java SE 21 Certification - Table of Contents
Master all exam topics with comprehensive study guides and practice examples.
Popular Posts
1Z0-830 Java SE 21 Developer Certification
Azure AI Foundry Hello World
Azure AI Agent Hello World
Foundry vs Hub Projects
Build Agents with SDK
Bing Web Search Agent
Function Calling Agent
Spring Boot + Azure Key Vault Hello World Example
Spring Boot + Elasticsearch + Azure Key Vault Example
Spring Boot Azure AD (Entra ID) OAuth 2.0 Authentication
Deploy Spring Boot App to Azure App Service
Secure Azure App Service using Azure API Management
Deploy Spring Boot JAR to Azure App Service
Deploy Spring Boot + MySQL to Azure App Service
Spring Boot + Azure Managed Identity Example
Secure Spring Boot Azure Web App with Managed Identity + App Registration
Elasticsearch 8 Security - Integrate Azure AD OIDC