Lock vs Synchronized
Lock and synchronized both provide mutual exclusion and memory visibility guarantees. However, they differ significantly in usage and capabilities.
Synchronized
The synchronized keyword offers a simpler syntax with built-in exception handling. It automatically acquires the lock when entering a synchronized block and releases it when exiting, evenif an exception occurs. Synchronized locks are reentrant, meaning a thread can acquire the same lock multiple times without deadlocking itself.
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Lock Interface
ReentrantLock implements the Lock interface and provides the same mutual exclusion and memory visibility as synchronized, with the addition of reentrant locking semantics. Lock implementations offer greater flexibility for handling lock unavailability scenarios.
The Lock interface supports:
- Unconditional lock acquisition
- Polling-based lock attempts
- Timed lock acquisition with deadlines
- Interruptible lock acquisition
public class SafeCounter {
private final ReentrantLock lock = new ReentrantLock();
private int value = 0;
public void increment() {
lock.lock();
try {
value++;
} finally {
lock.unlock();
}
}
}
ReentrantLock provides better liveness, performance tuning options, and configurable fairness policies compared to synchronized. However, explicit lock release in a final block is mandatory to prevent resource leaks.
AbstractQueuedSynchronizer Architecture
AbstractQueuedSynchronizer (AQS) serves as the foundation for building synchronization primitives and concurrent utilities. It manages synchronization state, maintains a queue of waiting threads, and coordinates lock acquisition and release.
Core Responsibilities
-
State Management: AQS encapsulates synchronization state within subclasses. The state represents the resource count or ownership status.
-
Queue Management: AQS maintains a FIFO queue of threads waiting to acquire the synchronization primitive. This queue handles thread scheduling and blocking.
-
Condition Support: Each Condition object maintains its own wait queue, separate from the main synchronization queue. This enables precise thread signaling and waiting semantics.
public class SimpleSemaphore extends AbstractQueuedSynchronizer {
public SimpleSemaphore(int permits) {
setState(permits);
}
@Override
protected int tryAcquireShared(int args) {
for (;;) {
int current = getState();
int next = current - args;
if (next < 0 || compareAndSetState(current, next)) {
return next;
}
}
}
@Override
protected boolean tryReleaseShared(int args) {
for (;;) {
int current = getState();
int next = current + args;
if (compareAndSetState(current, next)) {
return true;
}
}
}
}
Dual Queue Mechanism
AQS employs two distinct queues:
-
Sync Queue: The main queue holding threads that cannot acquire the lock due to unavailability.
-
Condition Queue: Each Condition instance maintains its own queue for threads waiting on specific conditions.
This dual-queue design allows fine-grained control over thread coordination and enables constructs like Semaphore and ReentrantLock to manage resource access efficiently.
ThreadPool Creation
ExecutorService provides the foundation for asynchronous task execution.
Using ThreadFactory
ThreadFactory factory = Executors.defaultThreadFactory();
ExecutorService pool = Executors.newCachedThreadPool(factory);
Specifying Pool Size
int cpuCores = Runtime.getRuntime().availableProcessors();
ExecutorService pool = Executors.newScheduledThreadPool(cpuCores);