0% found this document useful (0 votes)
15 views33 pages

Java Concurrent Programming Essentials

The document provides an overview of Java concurrent programming, emphasizing the importance of writing efficient, thread-safe applications in multi-core environments. It covers key concepts such as the Java Memory Model, synchronization techniques, and the use of atomic variables for lock-free concurrency. Additionally, it discusses various tools and frameworks like ExecutorService and ReentrantLock that facilitate effective thread management and resource optimization.

Uploaded by

aslı mikaelson
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
15 views33 pages

Java Concurrent Programming Essentials

The document provides an overview of Java concurrent programming, emphasizing the importance of writing efficient, thread-safe applications in multi-core environments. It covers key concepts such as the Java Memory Model, synchronization techniques, and the use of atomic variables for lock-free concurrency. Additionally, it discusses various tools and frameworks like ExecutorService and ReentrantLock that facilitate effective thread management and resource optimization.

Uploaded by

aslı mikaelson
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Java Concurrent Programming & Atomic Variables

Mastering the art of writing efficient, thread-safe Java applications in multi-core environments
What is Concurrent Programming?
Java concurrent programming is the discipline of writing programs that execute
multiple tasks simultaneously while preserving correctness and performance. Multi-Core
This paradigm has become essential in modern software development as
Exploits modern CPU architectures
applications need to maximize the capabilities of multi-core processors.

Concurrent programming enables applications to remain responsive while


handling multiple operations, scale to serve thousands of users simultaneously, Responsive
and fully exploit the parallel processing power available in today's hardware
Improves user experience
architectures.

Coordinated
Manages shared state safely

Thread t = new Thread(() -> [Link]("Hello from thread"));


[Link]();
Learning Objectives
01 02 03

Understand Concurrency Apply Synchronization Use Atomic Variables


Grasp fundamental concepts of parallel Master techniques for protecting shared Leverage lock-free data structures for high-
execution, thread lifecycles, and the Java state using locks, monitors, and coordination performance concurrent operations
Memory Model primitives
Why Concurrency Matters
The computing landscape has fundamentally shifted from increasing clock speeds to adding more processor cores. Modern applications
must embrace concurrent programming to deliver the performance users expect.

Concurrency enables multiple tasks to make progress simultaneously, whether that's handling thousands of web requests on a server,
keeping a desktop application responsive during intensive operations, or processing large datasets in parallel.

Hardware Evolution Server Scalability Responsive Interfaces


CPU cores have multiplied while clock Handle thousands of simultaneous Keep GUIs responsive while performing
speeds plateaued—concurrency connections efficiently without blocking background tasks and long-running
unlocks this parallel potential operations

ExecutorService ex = [Link](2);
[Link](() -> taskA());
[Link](() -> taskB());
[Link]();
The Java Memory Model (JMM)
The Java Memory Model is the foundation of concurrent programming in Java. It defines the rules that govern how threads interact through
memory, establishing crucial guarantees about visibility and ordering of operations across threads.

What It Governs Why It Matters


• When writes by one thread become visible to other threads Without understanding the JMM, concurrent programs may appear
• Which instruction reorderings are permitted by the compiler to work correctly during testing but fail unpredictably in production
and CPU due to subtle visibility and ordering issues.

• How synchronization establishes happens-before relationships


volatile boolean running = true;
• Guarantees around final fields and safe publication

The volatile keyword provides JMM guarantees for variable


visibility.
Visibility & Ordering Challenges

Write Operation Memory Barrier Read Operation


Thread A updates shared variable Ensures visibility across cores Thread B sees the updated value

Visibility ensures that updates made by one thread are eventually seen by other threads. Without proper synchronization, threads may
cache variables locally, leading to one thread never observing changes made by another.

Instruction reordering, performed by both compilers and CPUs for optimization, can cause operations to execute in a different order than
written in source code. This reordering is invisible in single-threaded programs but can break concurrent algorithms.

volatile int shared;


Critical insight: Most concurrency bugs
shared = 42; // immediately visible to other threads
stem from incorrect assumptions about
visibility and ordering.
The volatile modifier prevents caching and establishes visibility guarantees.
Threads: The Foundation
A thread represents an independent path of execution within a program. While your program has a main thread that starts when the
application launches, you can create additional threads to perform work in parallel.

Creating Threads Thread vs Task


Java provides multiple ways to create threads. The classic Modern practice separates the concept of a task (work to be done)
approach extends the Thread class and overrides its run() from a thread (execution context). This separation enables better
method: resource management through thread pools.
class MyThread extends Thread {
public void run() {
Always call start(), not run()—calling run() directly
[Link]("running");
executes on the current thread!
}
}
new MyThread().start();
Runnable: Task as Object

Functional Interface Lambda Compatible Reusable


Single abstract method: run() Perfect for concise task definitions Same task can run on multiple threads

The Runnable interface represents a task that can be executed by a thread but doesn't return a result. This separation of task from thread is
fundamental to modern concurrent programming, as it allows the same task logic to be executed by different threads or thread pools.

Traditional Approach Modern Lambda Syntax

Runnable r = new Runnable() { Runnable r = () -> {


public void run() { [Link]("task");
[Link]("task"); };
} new Thread(r).start();
};
new Thread(r).start();
Callable & Future
While Runnable is useful for fire-and-forget tasks, many concurrent operations need to return results. The Callable interface represents a
task that returns a value, and Future acts as a placeholder for that eventual result.

1 2 3 4

Submit Execute Complete Retrieve


Task submitted to executor Task runs on worker thread Result becomes available [Link]() returns value

Key Methods
Callable<Integer> c = () -> {
[Link](1000); • get() — blocks until result available
return 10;
• isDone() — checks completion status
};
• cancel() — attempts to cancel execution
ExecutorService executor =
[Link](); • get(timeout) — bounded waiting
Future<Integer> f = [Link](c);
Integer result = [Link](); // blocks until ready
Executor Framework
The Executor framework provides a high-level abstraction for managing thread pools and task execution. Instead of manually creating and
managing threads, you submit tasks to an executor service that handles the threading details.

This decoupling of task submission from execution policy enables you to easily change how tasks are executed—sequentially, in parallel
with a fixed pool, or with dynamically adjusted concurrency—without modifying the task code itself.

Executors ExecutorService ScheduledExecutorService


Factory class providing common Manages lifecycle and task submission Supports delayed and periodic task
executor configurations execution

ExecutorService pool = [Link](4);


[Link](() -> processTask());
[Link](); // graceful termination
Thread Pools: Efficient Resource Management
Thread pools reuse a fixed number of worker threads to execute tasks, avoiding the overhead of creating and destroying threads for each operation. This
pattern is essential for scalable server applications that handle thousands of concurrent requests.

90% 5000 4x
Overhead Reduction Task Queue Throughput Gain
Eliminating thread creation costs Pending tasks in typical pool Typical improvement vs new thread per task

Common Pool Types


ExecutorService pool =
• newFixedThreadPool(n) — fixed number of threads [Link](4);

• newCachedThreadPool() — grows and shrinks dynamically


[Link](() -> work());
• newSingleThreadExecutor() — serializes task execution
[Link](() -> work());
• newWorkStealingPool() — parallelism-based pool [Link](() -> work());

Choose pool size based on workload: CPU-bound tasks benefit


from core count, I/O-bound tasks from higher counts.
Synchronization: Protecting Shared State
Synchronization is the mechanism that prevents race conditions by ensuring that only one thread at a time can execute a critical section of code. Without
proper synchronization, concurrent access to shared mutable state leads to data corruption and unpredictable behavior.

1 2 3 4

Unsynchronized Acquire Lock Critical Section Release Lock


Multiple threads access shared data Thread obtains exclusive access Safe modification of shared state Other threads can now proceed
simultaneously, causing corruption

The Race Condition Problem Synchronized Solution

// BROKEN: not thread-safe // FIXED: thread-safe


class Counter { class Counter {
private int count = 0; private int count = 0;

public void increment() { public void increment() {


count++; // read-modify-write race synchronized(this) {
} count++;
} }
}
}
The synchronized Keyword
The synchronized keyword is Java's built-in mechanism for mutual exclusion. It can be applied to methods or blocks of code, ensuring that only one
thread can execute the protected code at a time by acquiring the associated monitor lock.

Method-Level Block-Level Static Synchronized

public synchronized synchronized(lock) { public static


void increment() { // critical section synchronized void reset() {
count++; count++; globalCount = 0;
} } }

Locks on the object instance (this) Explicit lock object for fine-grained control Locks on the Class object

How It Works
Best practice: Keep synchronized regions as
Every Java object has an associated monitor lock. When a thread enters a synchronized small as possible to minimize contention and
method or block, it must acquire the monitor. If another thread already holds it, the maximize concurrency.
requesting thread blocks until the lock becomes available.

The lock is automatically released when the thread exits the synchronized region, even if
an exception is thrown, preventing deadlocks from forgotten unlocks.
ReentrantLock: Explicit Locking
While synchronized is convenient, ReentrantLock provides more sophisticated locking capabilities. It offers the same mutual exclusion guarantees but
with additional features like timed lock attempts, interruptible locking, and multiple condition variables.

Basic Usage Pattern Advanced Capabilities

ReentrantLock lock = new ReentrantLock(); // Try to acquire lock with timeout


if ([Link](1, [Link])) {
[Link](); try {
try { // got lock, do work
// critical section } finally {
count++; [Link]();
} finally { }
[Link](); // always in finally } else {
} // couldn't get lock, handle failure
}

Critical: Always unlock in a finally block to prevent lock leaks!

Timed Acquisition Interruptible Fairness Policy


Attempt to acquire lock with timeout, Lock acquisition can be interrupted, enabling Optional fair mode ensures longest-waiting
preventing indefinite blocking responsive cancellation thread gets lock first
ReadWriteLock: Optimizing Reader Concurrency
The ReadWriteLock interface recognizes that many data structures are read far more often than they're written. It allows multiple threads to read
simultaneously while still ensuring exclusive access for writes, dramatically improving throughput for read-heavy workloads.

Read Lock Exclusive Write


Multiple readers acquire shared access Writer modifies data exclusively

1 2 3 4

Write Lock Resume Reads


Single writer waits for readers to complete Readers acquire lock after write completes

ReadWriteLock rwlock = new ReentrantReadWriteLock(); // Writing thread


[Link]().lock();
// Reading threads try {
[Link]().lock(); [Link](key, value);
try { } finally {
return [Link](key); [Link]().unlock();
} finally { }
[Link]().unlock();
}

Use case: Caches, configuration stores, and other data structures with infrequent updates but frequent reads benefit enormously from read-write locks.
StampedLock: Optimistic Concurrency
Introduced in Java 8, StampedLock offers an even more sophisticated locking mechanism with three modes: writing, reading, and
optimistic reading. The optimistic mode allows reads to proceed without any locking overhead, validating afterward that no write
occurred.
Optimistic Read Pattern When to Use
StampedLock excels in scenarios with very low write contention where optimistic reads
StampedLock sl = new StampedLock(); rarely need to retry. This makes it ideal for read-dominated data structures in low-
long stamp = [Link](); contention environments.
// read variables
if (![Link](stamp)) {
// write occurred, need real lock Performance: Can outperform ReadWriteLock by 2-3x in read-heavy
stamp = [Link](); workloads with minimal writes.
try {
// re-read variables
} finally {
[Link](stamp);
}
}

10x 0 3
Read Throughput Lock Overhead Lock Modes
Improvement over synchronized in read-heavy scenarios Optimistic reads have zero locking cost Write, read, and optimistic read
Atomic Variables: Lock-Free Concurrency
Atomic variables provide thread-safe operations on single variables without using locks. Built on top of compare-and-swap (CAS) CPU instructions, they
offer a lightweight, lock-free alternative to synchronization for simple operations.

Lock-Free High Performance Thread-Safe


No blocking, no deadlocks Lower overhead than locks Guaranteed atomic operations

Without Atomics With Atomics

// Requires synchronization // Lock-free


class Counter { class Counter {
private int count = 0; private AtomicInteger count = new AtomicInteger(0);

public synchronized void inc() { public void inc() {


count++; [Link]();
} }
} }

The [Link] package provides atomic classes for integers, longs, booleans, references, and arrays. These classes form the foundation for building
high-performance concurrent data structures.
AtomicInteger in Action
AtomicInteger provides thread-safe operations on integer values without requiring explicit synchronization. It's particularly useful for counters, sequence
generators, and other scenarios requiring atomic numeric updates.

incrementAndGet() getAndIncrement()
Atomically increment and return new value Return current value, then increment

int newVal = [Link](); int oldVal = [Link]();

addAndGet(delta) compareAndSet()
Add delta and return new value Update if value matches expectation

int result = [Link](5); [Link](10, 20);

Common Use Cases

AtomicInteger requestCounter = new AtomicInteger(0);


AtomicInteger activeUsers = new AtomicInteger(0);
// Thread-safe increment
[Link]();
// Thread-safe decrement
[Link]();
// Atomic update with function
[Link](n -> n * 2);
Compare-And-Swap (CAS)
Compare-And-Swap is the fundamental operation underlying all atomic variables. CAS atomically checks if a memory location contains an expected value and, if so, updates it to a
new value. This entire operation happens as a single indivisible instruction at the CPU level.

Compute New Value


Read Current Value
Thread calculates desired new value based on current value
Thread reads the current value from memory

Retry on Failure
Attempt CAS
If CAS fails due to concurrent modification, loop and retry
Atomically compare and swap if value hasn't changed

CAS Semantics Simple CAS Usage

boolean compareAndSet(int expectedValue, int newValue) { AtomicInteger ai = new AtomicInteger(5);


if (currentValue == expectedValue) {
currentValue = newValue; // Try to change from 5 to 10
return true; boolean success = [Link](5, 10);
} // success = true, ai is now 10
return false;
} // Try to change from 5 to 20
success = [Link](5, 20);
// success = false, ai still 10
CAS Loop: Optimistic Retry Pattern
When you need to perform complex atomic updates beyond simple increment/decrement, the CAS loop pattern provides a general solution. This optimistic approach assumes
success and retries when conflicts occur, avoiding locks entirely.

The Pattern Real-World Example

AtomicInteger ai = new AtomicInteger(0); // Atomically double the value


AtomicInteger counter = new AtomicInteger(10);
while (true) {
int current = [Link](); while (true) {
int newValue = computeNewValue(current); int prev = [Link]();
int doubled = prev * 2;
if ([Link](current, newValue)) {
// Success! Update applied if ([Link](prev, doubled)) {
break; [Link]("Updated to: " + doubled);
} break;
// CAS failed, another thread modified }
// the value. Loop and retry. }
}

99% 2/10
Success Rate Retry Average
First-try success in low contention Iterations needed under moderate contention

Key insight: CAS loops work well when contention is low to moderate. Under extreme contention, locks may actually perform better due to less wasted retry work.
AtomicReference: Lock-Free Object Updates
While AtomicInteger handles numeric values, AtomicReference enables atomic operations on object references. This allows you to build lock-free data
structures by atomically swapping entire objects rather than protecting field updates with locks.

Immutable Update Pattern


AtomicReference<String> ref = new AtomicReference<>("initial");
AtomicReference<List<String>> list = new AtomicReference<>( new ArrayList<>());
// Atomic update
[Link]("updated"); while (true) {
List<String> current = [Link]();
// Conditional atomic update List<String> updated =
boolean changed = [Link]( new ArrayList<>(current);
"updated", // expected [Link]("item");
"final" // new value if ([Link](current, updated)) {
); break;
}
// Atomic get-and-set }
String prev = [Link]("newest");

1 2 3

Create Immutable Copy Modify Copy CAS Swap


Clone the current object state Apply changes to the copy Atomically replace if original unchanged
The ABA Problem
The ABA problem is a subtle issue with CAS operations where a value changes from A to B and back to A between a thread's read and CAS attempt. The
CAS succeeds because it sees the expected value A, even though the value has actually been modified.
Thread 1 Reads Thread 2 Reverts
Sees value A, begins computation Updates B → A

1 2 3 4

Thread 2 Changes Thread 1 CAS


Updates A → B Succeeds but may be incorrect!

Why It Matters Example Scenario


In data structures like stacks or queues, the ABA problem can cause
// Stack: A → B → C
corruption. For example, if a stack pointer is updated from node A to B and
AtomicReference<Node> top = new AtomicReference<>(A);
back to A, a CAS operation might incorrectly succeed even though the
stack structure has fundamentally changed.
// Thread 1 reads A, gets preempted
Node observed = [Link]();

// Thread 2: pop A, pop B, push A


// Stack: A → C

// Thread 1: CAS succeeds!


// But [Link] now points to C, not B
// Data structure corrupted!

Simple data types: For plain integers and simple references without reuse, ABA is typically not a concern.
AtomicStampedReference: Solving ABA
AtomicStampedReference solves the ABA problem by pairing each reference with an integer stamp (version number). Now CAS operations require both
the reference and the stamp to match, detecting any intermediate changes even if the reference value cycles back.

Creating and Using How It Prevents ABA


Even if the reference value cycles A → B → A, the stamp will have changed (e.g., 0 → 1 → 2),
AtomicStampedReference<String> asr = new AtomicStampedReference<>(
causing the CAS to fail as expected. This maintains correctness in lock-free data structures.
"A", // initial reference
0 // initial stamp
// Thread 1: reads (A, 0)
);
int[] holder = new int[1];
String val = [Link](holder);
int[] stampHolder = new int[1];
String ref = [Link](stampHolder);
// Thread 2: (A,0) → (B,1) → (A,2)
int stamp = stampHolder[0];

// Thread 1: CAS fails!


// CAS with stamp check
// Expected stamp 0, actual stamp 2
boolean success = [Link](
[Link](val, "C", holder[0], holder[0]+1);
"A", // expected ref
"B", // new ref
stamp, // expected stamp
stamp + 1 // new stamp
);

AtomicMarkableReference
Simpler alternative using a boolean mark instead of an integer stamp when you only need to detect change, not count versions
LongAdder: High-Throughput Counters
LongAdder is a specialized class designed for high-contention scenarios where many threads frequently update a counter. Instead of all threads
competing for a single atomic value, it maintains multiple cells that threads can update independently, summing them only when the total is needed.

Split Counters Thread Affinity


Multiple independent cells reduce contention Each thread prefers updating its own cell

High Throughput Lazy Aggregation


Dramatically outperforms AtomicLong under contention Sum all cells only when total requested

When to Use
LongAdder adder = new LongAdder();
• High-frequency updates from many threads
// Increment from many threads • Reads are infrequent compared to writes
[Link]();
[Link](5); • Perfect for metrics, statistics, request counters

// Get current sum Tradeoff: sum() is not atomic and may not reflect all concurrent updates—use when
long total = [Link](); approximate counts are acceptable.

// Reset to zero
[Link]();
Concurrent Collections
The [Link] package provides thread-safe collection implementations optimized for concurrent access. Unlike
synchronized wrappers, these collections use sophisticated lock-free or fine-grained locking algorithms to maximize parallelism.

ConcurrentLinkedQueue ConcurrentHashMap ConcurrentSkipListMap


Lock-free unbounded queue using CAS Highly concurrent hash table with Sorted concurrent map with O(log n)
operations for high throughput segment-level locking operations

// Lock-free queue for producer-consumer


Key benefit: Concurrent collections allow
ConcurrentLinkedQueue<Task> queue =
multiple threads to read and write
new ConcurrentLinkedQueue<>();
simultaneously without blocking, unlike
[Link]().
// Producer
[Link](new Task());

// Consumer
Task task = [Link]();
ConcurrentHashMap: Scalable Thread-Safe Map
ConcurrentHashMap is one of the most important concurrent collections. Unlike Hashtable or [Link](), it allows concurrent reads
and uses fine-grained locking for writes, achieving excellent scalability.

16 100% 10x
Default Segments Read Concurrency Throughput Gain
Partitions for parallel writes Lock-free reads always vs synchronized HashMap under load

Basic Operations Atomic Compound Operations

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); // Atomic compute


[Link]("counter", (k, v) -> v == null ? 1 : v + 1);
// Thread-safe put
[Link]("key", 42); // Atomic update
[Link]("key", (k, v) -> v * 2);
// Thread-safe get
Integer val = [Link]("key"); // Parallel forEach
[Link](1, (k, v) -> process(k, v));
// Atomic putIfAbsent
Integer prev = [Link]("key", 100);

// Atomic remove
[Link]("key", 42);

Java 8+: ConcurrentHashMap offers rich atomic operations like compute(), merge(), and parallel bulk operations for functional-style concurrent programming.
Coordination Utilities
Beyond locks and atomic variables, Java provides high-level utilities for coordinating the execution phases of multiple threads. These synchronizers handle common patterns
like waiting for multiple tasks to complete or implementing phased parallel algorithms.

CountDownLatch CyclicBarrier
Makes threads wait until a count reaches zero Threads wait for each other at a barrier point

CountDownLatch latch = new CountDownLatch(3); CyclicBarrier barrier = new CyclicBarrier(3);


[Link](); // decrement [Link](); // wait for all threads
[Link](); // wait for zero

Exchanger Semaphore
Two threads exchange objects at a synchronization point Controls access to a resource pool with permits

Exchanger<String> ex = new Exchanger<>(); Semaphore sem = new Semaphore(5);


String received = [Link]("sent"); [Link](); // get permit
[Link](); // return permit
Fork/Join Framework
The Fork/Join framework provides a sophisticated infrastructure for parallel divide-and-conquer algorithms. It excels at recursively splitting problems into smaller subtasks,
processing them in parallel, and combining results.

Distribute
Fork
Worker threads steal subtasks from queue
Split task into independent subtasks

Join
Compute
Combine results from subtasks
Process subtasks in parallel

Work Stealing
ForkJoinPool pool = [Link]();
The framework uses work-stealing: idle worker threads
// Submit a recursive task steal tasks from busy threads' queues, ensuring balanced
Integer result = [Link]( load distribution and maximum CPU utilization.
new RecursiveTaskImpl(data)
);

// Or use common pool directly


RecursiveTaskImpl task = new RecursiveTaskImpl(data);
Integer result = [Link]().join();

Best for: Recursive algorithms like merge sort, tree traversals, parallel array operations, and other divide-and-conquer problems.
RecursiveTask: Fork/Join with Results
RecursiveTask<V> is the primary abstraction for tasks that return values in the Fork/Join framework. You extend this class and implement the compute()
method to define your parallel algorithm.
Pattern Structure Key Decisions

class Sum extends RecursiveTask<Long> { Threshold


private long[] array;
private int start, end; Size below which sequential processing is faster than forking
private static final int THRESHOLD = 1000;

protected Long compute() { Splitting


if (end - start <= THRESHOLD) {
// Base case: compute directly How to divide the problem into balanced subtasks
return computeDirectly();
} else {
// Recursive case: split Combining
int mid = (start + end) / 2; How to merge results from subtasks
Sum left = new Sum(array, start, mid);
Sum right = new Sum(array, mid, end);

[Link](); // async execute


long rightResult = [Link]();
long leftResult = [Link]();

return leftResult + rightResult;


}
}
}

RecursiveAction: Use RecursiveAction instead of RecursiveTask when your parallel computation doesn't need to return a value.
CompletableFuture: Asynchronous Programming
CompletableFuture revolutionizes asynchronous programming in Java by providing a rich API for composing, combining, and transforming asynchronous
operations. It represents a value that will be available in the future and supports non-blocking callbacks.

1 2 3 4

Supply Transform Combine Handle


Start async computation Chain transformations Merge multiple futures Process result or error

Creating Futures Combining Futures

// Async supplier CompletableFuture<String> f1 = fetchUser();


CompletableFuture<Integer> future = CompletableFuture<String> f2 = fetchOrders();
[Link](() -> {
// Long computation // Wait for both
return 42; [Link](f1, f2)
}); .thenRun(() ->
[Link]("Both done")
// Async runner );
CompletableFuture<Void> voidFuture =
[Link](() -> { // Use first completed
// Side effects [Link](f1, f2)
performTask(); .thenAccept(result ->
}); process(result)
);
CompletableFuture Pipeline
The real power of CompletableFuture emerges when chaining multiple asynchronous operations into declarative pipelines.
Each stage processes the result of the previous stage without blocking threads.

1 2 3

supplyAsync thenApply thenCompose


Initiate async computation returning a value Transform the result when available Chain to another async operation

thenAccept
Consume final result

Stage Variants
[Link](() -> {
return fetchUserId(); • thenApply — sync transformation
}) • thenApplyAsync — async transformation
.thenApply(id -> {
• thenAccept — consume result
return "User-" + id;
}) • thenRun — side effect action
.thenCompose(username -> { • thenCompose — flatten nested futures
return fetchUserDetails(username); • thenCombine — merge two futures
})
.thenAccept(details -> {
[Link](details);
})
.exceptionally(ex -> {
handleError(ex);
return null;
});

Non-blocking: The entire pipeline executes without blocking any threads, enabling highly scalable reactive applications.
ThreadLocal: Thread-Confined State
ThreadLocal provides each thread with its own independent copy of a variable. This enables thread-safe code without synchronization by eliminating
shared mutable state—each thread accesses only its own copy.

Basic Usage Common Use Cases


• User context in web applications (user ID, session)
ThreadLocal<Integer> threadId =
• Database connections per thread
[Link](() -> {
return generateId(); • Date formatters (SimpleDateFormat is not thread-safe)
}); • Transaction contexts
• Random number generators
// Each thread gets its own value
int id = [Link]();
[Link](newId);

// Clean up when done


[Link]();

No Synchronization Needed Memory Considerations


Each thread works with isolated data, eliminating contention and Remember to call remove() to prevent memory leaks, especially in
race conditions thread pools where threads are reused

Warning: ThreadLocal can cause memory leaks in thread pools if not properly cleaned up. Always call remove() when done.
Common Concurrency Pitfalls
Concurrent programming introduces subtle failure modes that don't exist in sequential code. Understanding these common pitfalls helps you write more robust concurrent
applications and debug issues when they arise.

Deadlocks Livelocks Starvation


Threads waiting for each other's locks in a cycle Threads continuously change state without progress Thread perpetually denied resources it needs

// Thread A: lock(X), lock(Y)


// Thread B: lock(Y), lock(X)
// → Deadlock!

Race Conditions Visibility Issues Memory Leaks


Timing-dependent bugs from uncoordinated access Threads not seeing updates without proper synchronization ThreadLocal values not cleaned up in thread pools

Prevention Strategies Tools for Detection


• Always acquire locks in consistent order Java provides several tools for analyzing concurrent behavior:
• Use timeouts for lock acquisition
• Prefer higher-level concurrency utilities // Thread dump
jstack <pid>
• Minimize scope of synchronized blocks
• Favor immutability and thread confinement // Heap dump
jmap -dump:file=[Link] <pid>

// Monitor contention
java -XX:+PrintConcurrentLocks

You might also like