Lock Types in Java Concurrency
Fair vs Unfair Locks
Fair locks ensure threads acquire locks in the order they requested them, preventing thread starvation but potentially reducing throughput. Unfair locks allow threads to acquire locks out of order, wich can improve performance but may lead to some threads waiting indefinitely.
// Creating a fair lock
ReentrantLock fairLock = new ReentrantLock(true);
// Default behavior: unfair lock
ReentrantLock defaultLock = new ReentrantLock();
Reentrant Locks
Both synchronized blocks and explicit Lock implementations in Java are reentrant. This means a thread that already holds a lock can acquire it again without deadlocking. The lock maintains a hold count that increments with each acquisition and decrements with each release.
Explicit Lock Implementation
Since JDK 5, Java provides explicit locking mechanisms through the Lock interface, offering more flexibility than synchronized blocks.
public class Worker implements Runnable {
private static final ReentrantLock accessLock = new ReentrantLock();
public void performTask() {
accessLock.lock();
try {
// Critical section code
System.out.println("Executing critical section");
} finally {
accessLock.unlock();
}
}
@Override
public void run() {
performTask();
}
}
Thread Interview Questions
Synchronized vs Lock Comparison
- Syntax: synchronized is keyword-based, Lock is API-based
- Flexibility: Lock allows try-lock, timed lock, and interruptible lock acquisition
- Condition Variables: Lock supports multiple Condition objects for finer-grained control
- Performance: Lock may offer better performance under high contention
Lock Release Scenarios
Locks are released when:
- Synchronized method/block completes normally
- Thread encounters return or break statement
- Uncaught exception or error occurs
- Thread calls wait() method
Locks are NOT released when:
- Thread calls sleep() or yield()
- Thread is suspended using deprecated suspend() method
Advanced Lock Exercise: Thread Coordination
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class PrintCoordinator {
private int sequence = 1;
private final ReentrantLock mutex = new ReentrantLock();
private final Condition firstCondition = mutex.newCondition();
private final Condition secondCondition = mutex.newCondition();
private final Condition thirdCondition = mutex.newCondition();
public void executeFirstPhase() {
mutex.lock();
try {
while (sequence != 1) {
firstCondition.await();
}
for (int i = 0; i < 3; i++) {
System.out.println("Phase 1 - Step " + (i + 1));
}
sequence = 2;
secondCondition.signal();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
mutex.unlock();
}
}
public void executeSecondPhase() {
mutex.lock();
try {
while (sequence != 2) {
secondCondition.await();
}
for (int i = 0; i < 2; i++) {
System.out.println("Phase 2 - Step " + (i + 1));
}
sequence = 3;
thirdCondition.signal();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
mutex.unlock();
}
}
public void executeThirdPhase() {
mutex.lock();
try {
while (sequence != 3) {
thirdCondition.await();
}
System.out.println("Phase 3 - Final Step");
sequence = 1;
firstCondition.signal();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
mutex.unlock();
}
}
}
Thread Communication
Inter-Thread Coordination Example
Using two threads to print numbers alternately demonstrates basic thread communication:
class NumberPrinter implements Runnable {
private int counter = 100;
@Override
public void run() {
while (counter > 0) {
synchronized (this) {
notifyAll();
System.out.println(Thread.currentThread().getName() + ": " + counter);
counter--;
if (counter > 0) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
}
}
Communication Methods
- wait(): Pauses current thread and releases lock
- notify(): Wakes one waiting thread
- notifyAll(): Wakes all waiting threads
sleep() vs wait()
Modern Thread Creation Approaches
Using Callable Interface
Callable offers advantages over Runnable:
- Returns a result
- Can throw checked exceptions
- Supports generics
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class DataProcessor implements Callable<string> {
@Override
public String call() throws Exception {
Thread.sleep(1000);
return "Processed data";
}
}
public class CallableExample {
public static void main(String[] args) throws Exception {
DataProcessor processor = new DataProcessor();
FutureTask<string> task = new FutureTask<>(processor);
new Thread(task).start();
// Non-blocking check
if (task.isDone()) {
System.out.println("Result: " + task.get());
}
// Blocking retrieval
String result = task.get();
System.out.println("Final result: " + result);
}
}
</string></string>
Thread Pool Implementation
Thread pools provide efficient thread management:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadPoolDemo {
public static void main(String[] args) throws Exception {
// Create fixed-size thread pool
ExecutorService pool = Executors.newFixedThreadPool(10);
// Submit Runnable task
pool.execute(() -> {
System.out.println("Running in pool: " + Thread.currentThread().getName());
});
// Submit Callable task
Future<string> future = pool.submit(() -> {
Thread.sleep(500);
return "Task completed";
});
System.out.println("Future result: " + future.get());
// Graceful shutdown
pool.shutdown();
}
}
</string>
Benefits of Thread Pools
- Performance: Reduces thread creation overhead
- Resource Management: Controls concurrent thread count
- Responsiveness: Improves application reaction time
- Monitoring: Provides centralized thread management