Java Multithreading Fundamentals and Concurrency Management
Parallelism vs Concurrency
Parallelism refers to multiple threads executing simultaneously across different processors at the exact same moment. Concurrency involves rapid context switching between threads on a single processor, creating the illusion of simultaneous execution.
Thread Lifecycle States
Java threads transition through seven distinct states:
- New: Thread instance created via Runnable implementation or Thread extension
- Runnable: Thread ready for CPU allocation after start() invocation; also reached when time slice expires or yield() is called
- Running: Thread actively executing code upon CPU time slice acquisition
- Waiting: Thread enters this state via wait(), join(), or park() calls; requires explicit awakening through notify(), notifyAll(), or unpark()
- Timed Waiting: Similar to waiting but with automatic timeout awakening; triggered by wait(timeout), join(timeout), sleep(), or parkUntil()
- Blocked: Occurs when thread fails to acquire synchronization lock or makes I/O requests; thread placed in synchronization queue
- Terminated: Final state when thread completes execution or terminates due to exceptions
Thread Creation Methods
Four primary approaches exist for thread instantiation:
- Thread Extension: Subclass Thread and override run() method
- Runnable Implementation: Implement Runnable interface and override run() method
- Callable with FutureTask: Implement Callable interface with call() method returning values
- Thread Pool Usage: Reuse existing threads through ExecutorService frameworks
Threading Mechanisms
The start() method initiates true multithreading, allowing concurrent execution without blocking the calling thread. Direct run() invocation executes sequentially within the current thread.
Thread Pool Advantages
Thread pools provide resource efficiency through reuse, faster response times, and improved managemant capabilities. They prevent excessive thread creation that could destabilize system performance.
ThreadPoolExecutor Configuration
Manual thread pool creation offers precise control over parameters:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // Core pool size
10, // Maximum pool size
1L, // Keep alive time
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);
Key parameters include core pool size, maximum capacity, queue configuration, and rejection policies.
Atomic Operations
The java.util.concurrent.atomic package provides thread-safe operations without traditional locking:
- Basic Types: AtomicInteger, AtomicLong, AtomicBoolean
- Arrays: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray
- References: AtomicReference, AtomicStampedReference
AtomicInteger example demonstrating common operations:
AtomicInteger counter = new AtomicInteger(0);
int currentValue = counter.get(); // Get current value
int previous = counter.getAndSet(5); // Set new value
int incremented = counter.getAndIncrement(); // Increment operation
boolean success = counter.compareAndSet(5, 10); // Compare and swap
Synchronization Primitives
CAS (Compare-And-Swap)
CAS operations prevent ABA issues through version stamping, ensuring data integrity during concurrent modifications.
AQS (Abstract Queued Synchronizer)
AQS forms the foundation for many concurrency utilities:
private volatile int state;
protected final int getState() { return state; }
protected final void setState(int newState) { state = newState; }
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
Synchronized Mechanism
Synchronized blocks utilize monitorenter/monitorexit bytecode instructions for method and block-level locking. Post-JDK 1.6 optimizations include biased, lightweight, and heavyweight lock states.
Example of double-checked locking pattern:
public class Singleton {
private volatile static Singleton instance;
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Lock Implementations
ReentrantLock provides explicit locking with fairness options:
Lock lock = new ReentrantLock(true); // Fair lock
lock.lock();
try {
// Critical section
} finally {
lock.unlock();
}
Memory Visibility
Volatile variables ensure immediate synchronization with main memory and prevent instruction reordering:
private volatile boolean flag = false;
private volatile int counter = 0;
Thread Communication
Key differences between sleep/yield and wait/join mechanisms:
- sleep(): Pauses current thread without releasing locks
- yield(): Temporarily surrenders CPU time slice
- wait(): Releases lock and waits for notification
- join(): Waits for target thread completion
Lock Classification
Optimistic vs Pessimistic
Optimistic locking assumes minimal conflict and uses CAS operations. Pessimistic locking anticipates conflicts and employs exclusive access.
Fairness Policies
Fair locks follow FIFO ordering while unfair locks allow opportunistic acquisition:
Lock fairLock = new ReentrantLock(true);
Lock unfairLock = new ReentrantLock(false);
Performance Optimization
Spin Locks
Lightweight synchronization for short-duration critical sections where context switching overhead exceeds spin duration.
Lock Elimination
JVM optimization removes unnecessary synchronization for thread-local objects:
public String concatenate(String s1, String s2) {
StringBuffer buffer = new StringBuffer();
buffer.append(s1);
buffer.append(s2);
return buffer.toString(); // Synchronization eliminated
}
Thread-Local Storage
ThreadLocal provides per-thread variable isolation:
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("thread-specific-value");
String value = threadLocal.get();
threadLocal.remove(); // Prevent memory leaks
Blocking Queue Implementation
ArrayBlockingQueue demonstrates producer-consumer patterns:
ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
queue.put(1); // Blocking put
Integer item = queue.take(); // Blocking take
Thread Pool Tuning
Optimal thread pool sizing depends on workload characteristics:
- CPU-bound tasks: Threads ≈ CPU cores + 1
- I/O-bound tasks: Threads ≈ 2 × CPU cores + 1
Formula: Thread Count = CPU Cores × Target Utilization × (1 + Wait Time / Service Time)