Search Tutorials


1Z0-830 Java SE 21 - Concurrency | JavaInUse

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
ConcurrentHashMap map = 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);
AtomicReference ref = 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