1. The ReentrantLock class: flexible locking
The synchronized keyword is great for basic cases: you can quickly and easily protect a method or a block of code. But sometimes you want more:
- Manage the lock explicitly (for example, try to acquire it and, if it fails — don’t wait).
- Separate “read” and “write” permissions for a resource.
- Interrupt waiting for a lock.
- Diagnose who acquired or released a lock and when.
For such tasks, we have the ReentrantLock and ReadWriteLock classes. They provide more control and capabilities than good old synchronized.
What is it, exactly?
ReentrantLock is a class that implements the Lock interface. It works roughly like synchronized, but with additional bells and whistles. The main difference is that lock management becomes explicit: you call lock() and unlock() yourself.
An interesting point — the word reentrant means that a thread can acquire the same lock several times in a row without causing mutual blocking. This is useful if a method calls itself recursively or works with a shared lock in a call chain.
Usage syntax
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int value = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // Acquire the lock
try {
value++;
} finally {
lock.unlock(); // Always release the lock!
}
}
public int getValue() {
lock.lock();
try {
return value;
} finally {
lock.unlock();
}
}
}
Note:
Calls to lock() and unlock() must always be wrapped with try...finally. If you forget to call unlock(), no other thread will be able to enter the protected block — you’ll end up with a permanent lock.
ReentrantLock capabilities
Try to acquire:
You can attempt to acquire the lock without waiting forever:
if (lock.tryLock()) {
try {
// Do work
} finally {
lock.unlock();
}
} else {
// Failed to acquire — do something else
}
Wait with timeout:
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
// Acquired within 100 ms
}
Check whether the lock is held:
if (lock.isLocked()) { ... }
Queue introspection, lock “fairness”, and other goodies.
3. Example: incrementing a counter with ReentrantLock
Let’s evolve our console application (for example, simulating processing orders from different threads). Let’s compare how it looks with synchronized and with ReentrantLock.
Example with synchronized
public class OrderCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Equivalent example with ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class OrderCounter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
What’s the benefit?
- You can try to acquire the lock and avoid waiting forever (tryLock()).
- You can implement more complex logic: for example, acquire multiple locks in a specific order (relevant for complex data structures).
- You can “unlock” from another place (but do this very carefully — always remember to call unlock()!).
4. ReadWriteLock: locking for reads and writes
What is it?
ReadWriteLock is not just a lock, but an intelligent access dispatcher. Its main implementation is ReentrantReadWriteLock, and it splits locks into two categories: read and write.
When threads are only reading data and nobody is changing anything, they can safely work together — reading doesn’t interfere with reading. But as soon as someone decides to make a change, everyone else must wait: writes allow only one participant and require exclusivity.
This approach is especially useful where there are many reads and few writes — for example, in a product catalog that users constantly browse but update only occasionally.
Usage syntax
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ProductCatalog {
private final Map<String, String> products = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void addProduct(String id, String name) {
rwLock.writeLock().lock();
try {
products.put(id, name);
} finally {
rwLock.writeLock().unlock();
}
}
public String getProduct(String id) {
rwLock.readLock().lock();
try {
return products.get(id);
} finally {
rwLock.readLock().unlock();
}
}
}
Example usage in our application
Suppose we have an orders database that all threads read (for example, to search for an order), but from time to time new orders arrive (a write operation).
import java.util.*;
import java.util.concurrent.locks.*;
public class OrderDatabase {
private final List<String> orders = new ArrayList<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
// Adding an order (requires writeLock)
public void addOrder(String order) {
rwLock.writeLock().lock();
try {
orders.add(order);
} finally {
rwLock.writeLock().unlock();
}
}
// Getting a copy of all orders (can be read in parallel)
public List<String> getOrders() {
rwLock.readLock().lock();
try {
// Return a copy to avoid races
return new ArrayList<>(orders);
} finally {
rwLock.readLock().unlock();
}
}
}
What’s happening?
- As long as nobody is writing, thousands of threads can read the order list simultaneously.
- As soon as one thread starts adding an order, reads are blocked to avoid inconsistent data.
5. Comparison: when to use what?
| Scenario | synchronized | ReentrantLock | ReadWriteLock |
|---|---|---|---|
| Simple synchronization | ✔ | ✔ | ✖ (overkill) |
| Need timeout/try-acquire | ✖ | ✔ | ✔ |
| Many reads, few writes | ✖ | ✖ | ✔ (significant gain) |
| Need diagnostics/metrics | ✖ | ✔ | ✔ |
| Re-entrant locking | ✔ | ✔ (reentrancy) | ✔ |
Takeaways:
- For simple cases — use synchronized.
- For flexibility — ReentrantLock.
- For “read often, write rarely” scenarios — ReadWriteLock.
6. Visualization: how ReadWriteLock works
flowchart LR
subgraph Reading
T1[Thread 1] -- Read --> Orders
T2[Thread 2] -- Read --> Orders
T3[Thread 3] -- Read --> Orders
end
subgraph Writing
T4[Thread 4] -- Write (addOrder) --> Orders
end
Orders[Order list]
style Orders fill:#f9f,stroke:#333,stroke-width:2px
As long as no thread is writing, everyone can read simultaneously. Once a write appears, the other threads wait for writeLock to finish.
7. Implementation details and nuances
“Fairness”
ReentrantLock and ReentrantReadWriteLock can be configured in “fair” mode: threads are served in queue order rather than “first come, first served”. This prevents thread starvation but may reduce performance.
Lock fairLock = new ReentrantLock(true); // true — fair mode
ReadWriteLock fairRWLock = new ReentrantReadWriteLock(true);
Potential pitfalls
- Forgotten unlock: If you don’t call unlock(), you’ll get a permanent lock. Always use try...finally.
- Exceptions inside a critical section: Even if an exception occurs in the block, the lock must still be released!
- Overusing ReadWriteLock: For small collections or when you almost always write, ReadWriteLock brings little benefit and makes the code more complex.
8. Common mistakes
Mistake #1: forgot to call unlock()
The most frequent and insidious error is to forget to call unlock() after acquiring a lock. The result is an endless block; threads “hang”. Always use try...finally, even if it seems “nothing can go wrong here”.
Mistake #2: using ReadWriteLock where it isn’t needed
If you have almost no parallel reads and writes are frequent, ReadWriteLock will only complicate the code and reduce performance. Use it only where there are truly many concurrent readers.
Mistake #3: acquiring multiple locks in different orders
If your code acquires several Locks (for example, for multiple objects), always acquire them in the same order in all threads. Otherwise, you can get a deadlock — threads will wait for each other forever.
Mistake #4: trying to replace synchronized with ReentrantLock “just because”
Do not mindlessly replace all synchronized with Lock — it doesn’t always speed up the program and can make the code less readable.
Mistake #5: forgot about reentrancy
If the same thread calls lock() several times in a row — that’s normal for ReentrantLock, but don’t forget that you must call unlock() the same number of times!
GO TO FULL VERSION