
Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:
Once the early-adopter seats are all used, the price will go up and stay at $33/year.
Last updated: March 26, 2025
Simply put, a lock is a more flexible and sophisticated thread synchronization mechanism than the standard synchronized block.
The Lock interface has been around since Java 1.5. It’s defined inside the java.util.concurrent.lock package, and it provides extensive operations for locking.
In this tutorial, we’ll explore different implementations of the Lock interface and their applications.
There are a few differences between the use of synchronized block and using Lock APIs:
Let’s take a look at the methods in the Lock interface:
A locked instance should always be unlocked to avoid deadlock condition.
A recommended code block to use the lock should contain a try/catch and finally block:
Lock lock = ...;
lock.lock();
try {
// access to the shared resource
} finally {
lock.unlock();
}
In addition to the Lock interface, we have a ReadWriteLock interface that maintains a pair of locks, one for read-only operations and one for the write operation. The read lock may be simultaneously held by multiple threads as long as there is no write.
ReadWriteLock declares methods to acquire read or write locks:
ReentrantLock class implements the Lock interface. It offers the same concurrency and memory semantics as the implicit monitor lock accessed using synchronized methods and statements, with extended capabilities.
Let’s see how we can use ReentrantLock for synchronization:
public class SharedObjectWithLock {
//...
ReentrantLock lock = new ReentrantLock();
int counter = 0;
public void perform() {
lock.lock();
try {
// Critical section here
count++;
} finally {
lock.unlock();
}
}
//...
}
We need to make sure that we are wrapping the lock() and the unlock() calls in the try-finally block to avoid the deadlock situations.
Let’s see how the tryLock() works:
public void performTryLock(){
//...
boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);
if(isLockAcquired) {
try {
//Critical section here
} finally {
lock.unlock();
}
}
//...
}
In this case, the thread calling tryLock() will wait for one second and will give up waiting if the lock isn’t available.
ReentrantReadWriteLock class implements the ReadWriteLock interface.
Let’s see the rules for acquiring the ReadLock or WriteLock by a thread:
Let’s look at how to make use of the ReadWriteLock:
public class SynchronizedHashMapWithReadWriteLock {
Map<String,String> syncHashMap = new HashMap<>();
ReadWriteLock lock = new ReentrantReadWriteLock();
// ...
Lock writeLock = lock.writeLock();
public void put(String key, String value) {
try {
writeLock.lock();
syncHashMap.put(key, value);
} finally {
writeLock.unlock();
}
}
...
public String remove(String key){
try {
writeLock.lock();
return syncHashMap.remove(key);
} finally {
writeLock.unlock();
}
}
//...
}
For both write methods, we need to surround the critical section with the write lock — only one thread can get access to it:
Lock readLock = lock.readLock();
//...
public String get(String key){
try {
readLock.lock();
return syncHashMap.get(key);
} finally {
readLock.unlock();
}
}
public boolean containsKey(String key) {
try {
readLock.lock();
return syncHashMap.containsKey(key);
} finally {
readLock.unlock();
}
}
For both read methods, we need to surround the critical section with the read lock. Multiple threads can get access to this section if no write operation is in progress.
StampedLock is introduced in Java 8. It also supports both read and write locks.
However, lock acquisition methods return a stamp that is used to release a lock or to check if the lock is still valid:
public class StampedLockDemo {
Map<String,String> map = new HashMap<>();
private StampedLock lock = new StampedLock();
public void put(String key, String value){
long stamp = lock.writeLock();
try {
map.put(key, value);
} finally {
lock.unlockWrite(stamp);
}
}
public String get(String key) throws InterruptedException {
long stamp = lock.readLock();
try {
return map.get(key);
} finally {
lock.unlockRead(stamp);
}
}
}
Another feature provided by StampedLock is optimistic locking. Most of the time, read operations don’t need to wait for write operation completion, and as a result of this, the full-fledged read lock isn’t required.
Instead, we can upgrade to read lock:
public String readWithOptimisticLock(String key) {
long stamp = lock.tryOptimisticRead();
String value = map.get(key);
if(!lock.validate(stamp)) {
stamp = lock.readLock();
try {
return map.get(key);
} finally {
lock.unlock(stamp);
}
}
return value;
}
The Condition class provides the ability for a thread to wait for some condition to occur while executing the critical section.
This can occur when a thread acquires the access to the critical section but doesn’t have the necessary condition to perform its operation. For example, a reader thread can get access to the lock of a shared queue that still doesn’t have any data to consume.
Traditionally Java provides wait(), notify() and notifyAll() methods for thread intercommunication.
Conditions have similar mechanisms, but we can also specify multiple conditions:
public class ReentrantLockWithCondition {
Stack<String> stack = new Stack<>();
int CAPACITY = 5;
ReentrantLock lock = new ReentrantLock();
Condition stackEmptyCondition = lock.newCondition();
Condition stackFullCondition = lock.newCondition();
public void pushToStack(String item){
try {
lock.lock();
while(stack.size() == CAPACITY) {
stackFullCondition.await();
}
stack.push(item);
stackEmptyCondition.signalAll();
} finally {
lock.unlock();
}
}
public String popFromStack() {
try {
lock.lock();
while(stack.size() == 0) {
stackEmptyCondition.await();
}
return stack.pop();
} finally {
stackFullCondition.signalAll();
lock.unlock();
}
}
}
In this article, we saw different implementations of the Lock interface and the newly introduced StampedLock class.
We also explored how we can make use of the Condition class to work with multiple conditions.