java.util.concurrent package in Java 5. Among its most powerful components is the java.util.concurrent.locks package, commonly known as the Locks Framework.
This framework provides explicit locking mechanisms that offer greater flexibility and control than intrinsic locks used by the
synchronized keyword.
In this article, we will explore the Locks Framework in depth, understand why it was introduced, learn about the
Lock interface and ReentrantLock, and discover advanced locking features such as fairness policies, non-blocking lock acquisition and interruptible locking.
Limitations of "synchronized" keyword
Although thesynchronized keyword remains an essential part of Java, it was intentionally designed to provide only a simple mutual exclusion mechanism. As concurrent applications become larger and more performance-sensitive, several limitations become apparent.
1. No Timeout Support: When a thread attempts to enter a synchronized block whose lock is already held by another thread, it waits indefinitely until the lock becomes available.
synchronized (lock) {
// waits until lock becomes available
}
There is no mechanism to specify a timeout or abandon the lock acquisition attempt after waiting for a certain period. In high-concurrency applications, this may reduce responsiveness because blocked threads remain idle until the lock is released.
2. No Interruptible Lock Waiting: Interruptible lock waiting in Java is a concurrency feature that allows a blocked thread, which is currently waiting to acquire a lock, to break out of its waiting state if another thread interrupts it.
Instead of staying permanently blocked until the lock becomes free, the waiting thread immediately aborts its attempt, wakes up, and throws an InterruptedException to handle the cancellation gracefully.
If a thread tries to enter a synchronized block or method and the lock is held by another thread, it enters a blocked state. You cannot force it to stop waiting. Even if you call thread.interrupt(), the thread ignores the signal and remains stuck.
By using the explicit Lock API via its lockInterruptibly() method, a thread remains responsive to interruption while queuing for the lock.
3. No Fairness Policy: When multiple threads are waiting for the same intrinsic lock, Java does not guarantee the order in which they will acquire it. A thread that has been waiting the longest is not necessarily the next one to obtain the lock.
Instead, the JVM's thread scheduler decides which waiting thread gets the lock when it becomes available, and this decision is implementation-dependent.
As a result, some threads may repeatedly lose the race to acquire the lock and experience thread starvation under heavy contention. This lack of fairness can reduce the predictability of concurrent applications and negatively impact response times for waiting threads.
4. Limited Locking Features: Intrinsic locks provide only basic mutual exclusion. They do not support several advanced features commonly required in enterprise applications, including:
- Timed lock acquisition
- Non-blocking lock attempts
- Interruptible lock waiting
- Multiple condition variables
- Explicit lock management
These capabilities become increasingly important as concurrent systems grow more complex.
Before Java 5, concurrent programming primarily relied on theThreadclass, thesynchronizedkeyword, and low-level communication methods such aswait(),notify(), andnotifyAll().
Although these APIs made multithreading possible, building highly concurrent applications using only these primitives was often complicated and error-prone.
To solve these challenges, Java introduced thejava.util.concurrentpackage.
It contains a comprehensive collection of concurrency utilities, including executors, thread pools, concurrent collections, synchronization aids, atomic classes, asynchronous programming constructs, and the Locks Framework.
The Locks Framework
To overcome the limitations of intrinsic locking, Java introduced the Locks Framework inside thejava.util.concurrent.locks package.
Instead of allowing the JVM to manage lock acquisition automatically, developers acquire and release locks explicitly through well-defined APIs.
The framework introduces several powerful locking implementations, including:
- ReentrantLock
- ReadWriteLock
- StampedLock
These classes offer advanced synchronization capabilities while maintaining thread safety.
The Lock Interface
The foundation of the Locks Framework is thejava.util.concurrent.locks.Lock interface.
This interface defines the operations required for acquiring and releasing explicit locks. Unlike
synchronized, where lock management is handled automatically by the JVM, the Lock interface gives developers complete control over how locks are used.
Some of its most important methods include:
void lock();
void unlock();
boolean tryLock();
boolean tryLock(long time, TimeUnit unit);
void lockInterruptibly();
ReentrantLock
The most widely used implementation of theLock interface is ReentrantLock. It behaves similarly to the synchronized keyword but provides many advanced capabilities that make it suitable for complex concurrent applications.
A
ReentrantLock is created as follows:
import java.util.concurrent.locks.ReentrantLock;
ReentrantLock lock = new ReentrantLock();
Unlike intrinsic locks, a thread must explicitly acquire and release a ReentrantLock.
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
When lock.lock() is called, the thread attempts to acquire the lock. If another thread already owns the lock, the current thread waits until it becomes available.
Once the critical section completes,
lock.unlock() releases the lock so that another waiting thread may proceed.
Unlike
synchronized, explicit locks are not released automatically. Therefore, every call to lock() should always be paired with an unlock() inside a finally block.
Failing to release a lock prevents other threads from acquiring it and can lead to severe application issues such as deadlocks or permanently blocked threads.
Reentrancy
A reentrant lock allows the same thread to acquire the same lock multiple times without blocking itself.This capability is particularly useful when synchronized methods invoke other synchronized methods that require the same lock. Without reentrancy, a thread could deadlock itself by attempting to reacquire a lock it already holds.
Internally,
ReentrantLock maintains a hold count, which keeps track of how many times the current thread has acquired the lock. Each successful call to lock() increments this count, and every corresponding call to unlock() decrements it.
The lock is released for other threads only when the hold count reaches zero.
Example
import java.util.concurrent.locks.ReentrantLock;
class Example {
private final ReentrantLock lock = new ReentrantLock();
public void methodA() {
lock.lock();
try {
System.out.println("Inside methodA");
methodB();
} finally {
lock.unlock();
}
}
public void methodB() {
lock.lock();
try {
System.out.println("Inside methodB");
} finally {
lock.unlock();
}
}
}
In this example, methodA() acquires the lock and then invokes methodB(). Since the same thread already owns the lock, methodB() can safely acquire it again without blocking.
Internally, the hold count increases to two. When
methodB() completes, its call to unlock() decreases the hold count to one, and the final unlock() in methodA() reduces it to zero, making the lock available for other threads.
The intrinsic locks used by thesynchronizedkeyword are also reentrant. This means the following code works correctly as well:Here, the thread enteringclass Example { public synchronized void methodA() { methodB(); } public synchronized void methodB() { System.out.println("Reentrant synchronized"); } }methodA()already owns the object's monitor lock. WhenmethodB()is called, the same thread is allowed to acquire the monitor lock again because intrinsic locks are also reentrant.
Fair vs Non-Fair Locks
ReentrantLock supports both fair and non-fair locking, allowing developers to choose between maximum throughput and predictable thread scheduling.
By default, a
ReentrantLock is non-fair:
ReentrantLock lock = new ReentrantLock();
In non-fair mode, threads compete for the lock whenever it becomes available. A thread that has just arrived may acquire the lock before threads that have been waiting for a longer time.
This behavior, often called barging, improves overall throughput because it minimizes scheduling overhead and keeps the CPU busy. However, under heavy contention, some waiting threads may repeatedly lose the race to acquire the lock, leading to thread starvation.
A fair lock can be created by passing
true to the constructor:
ReentrantLock lock = new ReentrantLock(true);
With a fair lock, threads are generally granted access in the order they requested the lock, following a First-In-First-Out (FIFO) policy. This greatly reduces the possibility of starvation because threads that have been waiting the longest are given priority over newly arriving threads.
Although fair locks improve predictability, they introduce additional scheduling overhead because the JVM must maintain and honor the waiting queue.
tryLock()
One of the biggest limitations of thesynchronized keyword is that a thread attempting to acquire a lock must wait indefinitely until the lock becomes available.
To address this limitation,
ReentrantLock provides the tryLock() method. Instead of blocking the current thread, tryLock() attempts to acquire the lock immediately. If the lock is available, it returns true and the thread proceeds to execute the critical section.
If another thread already holds the lock, the method simply returns
false, allowing the application to perform an alternative action instead of waiting indefinitely.
import java.util.concurrent.locks.ReentrantLock;
public class Example {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
if (lock.tryLock()) {
try {
System.out.println("Lock acquired");
} finally {
lock.unlock();
}
} else {
System.out.println("Could not acquire lock");
}
}
}
In this example, the thread immediately attempts to acquire the lock. If successful, it executes the critical section and releases the lock in the finally block. If the lock is unavailable, the thread continues executing without blocking, allowing the application to remain responsive.
Timed tryLock()
Sometimes an application should wait for a short period before giving up. For such scenarios,ReentrantLock provides a timed version of tryLock().
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class Example {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
System.out.println("Lock acquired");
} finally {
lock.unlock();
}
} else {
System.out.println("Could not acquire lock within 2 seconds");
}
}
}
Here, the thread waits for up to two seconds to acquire the lock. If the lock becomes available during that time, execution continues normally. Otherwise, the method returns false, allowing the application to handle the timeout gracefully.
lockInterruptibly()
Another significant limitation of thesynchronized keyword is that a thread waiting to acquire an intrinsic lock cannot respond immediately to interruption requests. Even if another thread calls interrupt(), the waiting thread continues to remain blocked until the lock becomes available.
ReentrantLock addresses this problem through the lockInterruptibly() method. This method allows a thread waiting for a lock to be interrupted. If an interruption occurs while the thread is waiting, an InterruptedException is thrown, allowing the thread to terminate or perform cleanup immediately.
import java.util.concurrent.locks.ReentrantLock;
public class Example {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
try {
lock.lockInterruptibly();
try {
System.out.println("Lock acquired");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("Interrupted while waiting for lock");
}
}
}
If another thread interrupts the current thread while it is waiting for the lock, lockInterruptibly() immediately throws an InterruptedException. This allows the thread to stop waiting and respond promptly instead of remaining blocked indefinitely.
In the next article, we will explore ReadWriteLock and StampedLock, and learn how they significantly improve performance in read-heavy concurrent workloads by allowing multiple readers to access shared data simultaneously while still ensuring thread safety during updates.