synchronized in Java is the simplest built-in lock — it provides mutual exclusion and visibility guarantees.
For more control like timeout, try-lock, or interruptible waiting —► Use java.util.concurrent.locks.Lock.
Guarantee of sequence − synchronized block does not provide any guarantee of sequence in which waiting thread will be given access. Lock interface handles it.
Timeout − synchronized block has no option of timeout if lock is not granted. Lock interface provides such option.
synchronized block must be fully contained within a single method whereas a lock interface's methods lock() and unlock() can be called in different methods.
Note: lock.lock() and lock.unlock() work similarly to entering and exiting a synchronized block.
lock(): Acquires the lock — blocks until available.
tryLock(): Tries to acquire lock; returns false if not available (non-blocking).
tryLock(long time, TimeUnit unit): Waits up to a timeout.
lockInterruptibly(): Acquires the lock, but can be interrupted.
unlock(): Releases the lock.
ReentrantLock: A thread can acquire it multiple times (reentrant).
ReentrantReadWriteLock: Separates read and write locks — multiple readers allowed, only one writer.
StampedLock: For high-performance read/write operations (with optimistic reads).
✅ Good pattern:
Lock lock = new ReentrantLock();
try {
lock.lock();
// critical section
} finally {
lock.unlock();
}
It’s called Reentrant because a thread can re-acquire the same lock it already holds — just like with synchronized.
void nested() {
lock.lock();
try {
lock.lock(); // same thread can acquire again
try {
System.out.println("Reentrant!");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}
Without reentrancy, this would deadlock itself (the same thread waiting for a lock it already holds).
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
// critical section
} finally {
lock.unlock();
}
} else {
System.out.println("Couldn't get the lock, skipping task");
}
This is non-blocking locking — prevents deadlocks and improves responsiveness.
The longest-waiting thread gets the lock first (FIFO order).
ReentrantLock fairLock = new ReentrantLock(true);
Fair locks reduce starvation but can have lower throughput.
Condition works like wait() and notify(), but more explicit.
Create Condition objects from a ReentrantLock object (Note: Condition is specific to the lock)
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Can have multiple conditions for different states:
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
Key Methods of Condition:
await(): Causes current thread to wait until signaled or interrupted (equivalent to wait() in synchronized)
await(long time, TimeUnit unit): Waits for signal or timeout (equivalent to wait(timeout) in synchronized)
signal(): Wakes up one waiting thread (equivalent to notify() in synchronized)
signalAll(): Wakes up all waiting threads (equivalent to notifyAll() in synchronized)
👉 Note: All these must be called within a lock.lock() / unlock() block, or else a IllegalMonitorStateException is thrown — just like calling wait() without synchronization.
Variations of await():
awaitUninterruptibly(): Waits, but ignores interrupts.
awaitNanos(long nanosTimeout): Waits for a precise time in nanoseconds.
awaitUntil(Date deadline): Waits until a specific deadline time.
Example:
class SharedResource {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean available = false;
public void produce() throws InterruptedException {
lock.lock();
try {
while (available) condition.await(); // wait
System.out.println("Producing...");
available = true;
condition.signal(); // notify consumer
} finally {
lock.unlock();
}
}
public void consume() throws InterruptedException {
lock.lock();
try {
while (!available) condition.await(); // wait
System.out.println("Consuming...");
available = false;
condition.signal(); // notify producer
} finally {
lock.unlock();
}
}
}
Condition objects let you manage multiple wait-sets more cleanly than wait()/notify().
Multiple readers can hold the lock simultaneously — as long as no writer holds it.
Only one writer can hold the lock — and it has exclusive access (no readers or other writers).
+--------------------------+
| ReentrantReadWriteLock |
+--------------------------+
/ \
[readLock] [writeLock]
↑ ↑ ↑ ↑
T1 T2 T3 (many) T4 (only one)
Multiple readers (T1, T2, T3) can enter concurrently.
If a writer (T4) requests the lock:
it must wait until all readers finish.
new readers are blocked until the writer is done.
readLock(): Returns the lock for reading.
writeLock(): Returns the lock for writing.
lock() / unlock(): Acquire/release the lock (same as other Locks).
tryLock(): Try acquiring lock without blocking.
lockInterruptibly(): Acquire lock but respond to interrupts.
Reentrant: The same thread can acquire the same read or write lock multiple times.
Upgradeable:
A thread holding the write lock can also acquire read locks.
But a thread holding read lock cannot acquire write lock (to prevent deadlocks).
Fairness Option: Ensures FIFO lock granting
ReadWriteLock lock = new ReentrantReadWriteLock(true);
import java.util.concurrent.locks.*;
class SharedData {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private int data = 0;
public void write(int value) {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " writing " + value);
data = value;
Thread.sleep(500); // simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
writeLock.unlock();
}
}
public void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " reading " + data);
Thread.sleep(200); // simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
readLock.unlock();
}
}
}