Locks in Java
The Lock interface in java.util.concurrent.locks is an alternative to synchronized that gives more control. You take and release the lock explicitly, you can try to get a lock without waiting forever, you can make a lock fair, and you can be interrupted while waiting. The package has three main lock types: ReentrantLock, ReentrantReadWriteLock (many readers or one writer) and StampedLock (with fast optimistic reads). In this lesson, you will learn all three and when each is the right choice.
Methods of Lock
| Method | Description |
|---|---|
lock() | It is used to take the lock, waiting as long as needed. |
unlock() | It releases the lock; must be called, usually in finally. |
tryLock() | It takes the lock only if it's free right now; returns true or false. |
tryLock(time, unit) | It waits at most the given time. |
lockInterruptibly() | It waits, but can be interrupted. |
newCondition() | It creates a Condition for waiting and signaling (see "Condition Interface in Java"). |
The Standard Pattern
lock.lock();
try {
// use the shared data
} finally {
lock.unlock();
}Unlike synchronized, nothing releases a Lock automatically: if an exception skips unlock(), every other thread waits forever. The finally block guarantees the release.
Example: ReentrantLock with tryLock
Let's see an example where two people try to use one printer; the second gives up instead of waiting.
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class Printer {
private final ReentrantLock lock = new ReentrantLock();
void print(String who) throws InterruptedException {
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
System.out.println(who + " is printing");
Thread.sleep(300);
} finally {
lock.unlock();
}
} else {
System.out.println(who + " gave up: printer busy");
}
}
public static void main(String[] args) throws InterruptedException {
Printer printer = new Printer();
Thread a = new Thread(() -> run(printer, "Navika"));
Thread b = new Thread(() -> run(printer, "Srihitha"));
a.start();
Thread.sleep(50);
b.start();
a.join();
b.join();
System.out.println("Fair lock? " + printer.lock.isFair()
+ ", locked now? " + printer.lock.isLocked());
}
static void run(Printer p, String who) {
try {
p.print(who);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}Navika is printing
Srihitha gave up: printer busy
Fair lock? false, locked now? falseExplanation: Navika takes the lock and prints for 300 milliseconds. Srihitha tries 50 milliseconds later with tryLock(100 ms); the lock is still busy after 100 milliseconds, so the call returns false and Srihitha gives up instead of waiting. synchronized can't do this. new ReentrantLock(true) would create a fair lock that serves waiting threads in arrival order.
ReadWriteLock: Many Readers or One Writer
When data is read far more often than written, a ReentrantReadWriteLock lets many readers hold the read lock at once, while a writer gets the write lock alone:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class PriceList {
private final Map<String, Integer> prices = new HashMap<>();
private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
Integer get(String item) {
rw.readLock().lock();
try {
return prices.get(item);
} finally {
rw.readLock().unlock();
}
}
void put(String item, int price) {
rw.writeLock().lock();
try {
prices.put(item, price);
} finally {
rw.writeLock().unlock();
}
}
public static void main(String[] args) throws InterruptedException {
PriceList list = new PriceList();
list.put("notebook", 60);
long start = System.nanoTime();
Thread[] readers = new Thread[4];
for (int i = 0; i < readers.length; i++) {
readers[i] = new Thread(() -> {
list.rw.readLock().lock();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
list.rw.readLock().unlock();
}
});
readers[i].start();
}
for (Thread r : readers) r.join();
long ms = (System.nanoTime() - start) / 1_000_000;
System.out.println("notebook costs " + list.get("notebook"));
System.out.println("4 readers shared the lock (under 400 ms)? "
+ (ms < 400));
}
}notebook costs 60
4 readers shared the lock (under 400 ms)? trueExplanation: Four readers each held the read lock for 200 milliseconds, and the whole thing took a little over 200, because readers don't block each other. A writer would wait until all readers left, and readers would wait while a writer held the lock.
StampedLock
StampedLock (Java 8) adds optimistic reading: a reader takes a cheap "stamp" without locking, reads the data, then calls validate(stamp) to check that no write happened in between. If one did, it falls back to a real read lock. It's very fast for read-mostly data, but it isn't reentrant and is harder to use correctly.
Comparing the Locks
synchronized | ReentrantLock | ReentrantReadWriteLock | StampedLock | |
|---|---|---|---|---|
| Release | Automatic | Manual in finally | Manual | Manual (by stamp) |
| Try with timeout | No | Yes | Yes | Yes |
| Fairness option | No | Yes | Yes | No |
| Many readers at once | No | No | Yes | Yes (+ optimistic) |
| Reentrant | Yes | Yes | Yes | No |
| Conditions | wait/notify | newCondition() | Write lock only | No |
Common Mistakes
Calling unlock() outside finally. An exception leaves the lock held forever.
Calling lock() inside the try. If lock() fails, finally calls unlock() on a lock you don't hold; call it just before try.
Trying to upgrade a read lock to a write lock in ReentrantReadWriteLock. It deadlocks; release the read lock first.
Interview Corner
What is the difference between synchronized and ReentrantLock?
Both provide reentrant mutual exclusion with the same memory-visibility guarantees. synchronized is built into the language and releases the lock automatically when the block exits. ReentrantLock requires an explicit unlock(), usually in finally, but adds tryLock (with or without a timeout), lockInterruptibly, an optional fairness policy, multiple Condition objects, and methods to query the lock's state. Use synchronized by default and ReentrantLock when those extra features are needed.
Practice
- Easy: Rewrite a synchronized counter with a
ReentrantLock, using the lock-try-finally pattern. - Harder: Write a
transfermethod between two accounts that usestryLockwith a timeout on both accounts' locks, retrying after a short random pause if it can't get both, so it never deadlocks.
Key Takeaways
Was this tutorial helpful?