Java ReadWriteLock
Table of Contents + β
In the last lesson you learned about the Java Lock interface and ReentrantLock. A ReentrantLock lets only one thread in at a time, treating reads and writes the same way. That feels wasteful when most threads only want to look at the data. Javaβs answer is the ReadWriteLock, which splits one lock into two so readers can share.
π€ The problem: readers donβt need to wait for each other
Some data is read all the time but changed almost never. Think exchange rates, app settings, or a cache of user profiles. A plain lock treats every reader like a writer:
- A plain lock lets only one thread in at a time.
- So reader two waits for reader one, even though both only look.
- Reading together is always safe, because nobody is changing the data.
- You only need exclusive access when a thread writes.
- Making readers wait buys correctness you did not need and costs speed you did want.
The example below shows the slow pattern. Every call takes the same exclusive lock, even the read:
import java.util.concurrent.locks.ReentrantLock;
class Config { private final ReentrantLock lock = new ReentrantLock(); private String value = "default";
public String read() { lock.lock(); // β readers block each other for no reason try { return value; } finally { lock.unlock(); } }
public void write(String newValue) { lock.lock(); try { value = newValue; } finally { lock.unlock(); } }}The write method holding an exclusive lock is fine. The read method holding one is the waste. We need a lock that knows the difference between reading and writing.
π An analogy: a whiteboard in a meeting room
Picture a whiteboard on a wall. The board is your shared data:
- Readers crowd around and copy what it says. Many can read at once, because reading changes nothing.
- A writer erases the board and writes something new. Everyone else must step back, or they copy half-erased nonsense.
That is a ReadWriteLock. Many readers share the read side. One writer owns the write side alone. But readers never wait for other readers. That is the whole win.
π§© What is a ReadWriteLock?
A ReadWriteLock is not one lock. It is a pair that work together:
- The read lock is shared. Any number of threads can hold it, as long as no thread holds the write lock.
- The write lock is exclusive. Only one thread holds it, and while it is held, nobody can hold the read lock.
- The rule that ties it together: read locks share with each other, the write lock shares with nothing.
The standard implementation is ReentrantReadWriteLock, in java.util.concurrent.locks. You create one object, then ask it for its two locks:
import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;
ReadWriteLock rwLock = new ReentrantReadWriteLock();rwLock.readLock(); // the shared read lockrwLock.writeLock(); // the exclusive write lockThe word reentrant means the same thread can take a lock it already holds without freezing itself. So a thread holding the write lock can take it again safely.
π Using the read and write locks
You use each lock like ReentrantLock: call lock(), do the work, unlock() in a finally block. The finally releases the lock even on an exception, so no thread waits forever.
The example below protects a shared number. Reads use the read lock, the write uses the write lock:
import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;
class Counter { private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); private int count = 0;
public int get() { rwLock.readLock().lock(); // many readers allowed together try { return count; } finally { rwLock.readLock().unlock(); // β
always unlock in finally } }
public void increment() { rwLock.writeLock().lock(); // only one writer, alone try { count++; } finally { rwLock.writeLock().unlock(); // β
always unlock in finally } }}Walking through it:
gettakes the shared read lock, so five threads callinggetrun together.incrementtakes the exclusive write lock, so while one thread writes, nobody reads or writes.- Each method unlocks in
finally, so an exception never leaves a lock stuck. - Result: frequent reads no longer block each other, rare writes still get full safety.
ποΈ A worked example: a thread-safe cache
The classic use is a cache or config read often and written rarely. The class below stores settings in a map. Reading takes the read lock, so lookups run at once. Updating takes the write lock, so it happens alone:
import java.util.HashMap;import java.util.Map;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;
class SettingsCache { private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); private final Map<String, String> data = new HashMap<>();
public String get(String key) { rwLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + " reads " + key); return data.get(key); } finally { rwLock.readLock().unlock(); } }
public void put(String key, String value) { rwLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + " writes " + key); data.put(key, value); } finally { rwLock.writeLock().unlock(); } }}
public class Main { public static void main(String[] args) throws InterruptedException { SettingsCache cache = new SettingsCache(); cache.put("theme", "dark");
Runnable reader = () -> { for (int i = 0; i < 2; i++) cache.get("theme"); };
Thread r1 = new Thread(reader, "reader-1"); Thread r2 = new Thread(reader, "reader-2"); Thread w1 = new Thread(() -> cache.put("theme", "light"), "writer-1");
r1.start(); r2.start(); w1.start(); r1.join(); r2.join(); w1.join();
System.out.println("Final theme: " + cache.get("theme")); }}Here the main thread puts an initial theme. Two reader threads look it up twice each, and one writer changes it. Readers run together, the writer goes alone. The exact order shifts from run to run, but the writer is always exclusive:
Output
reader-1 reads themereader-2 reads themewriter-1 writes themereader-1 reads themereader-2 reads themeFinal theme: lightThe two readers overlap freely, while the writer always gets the data to itself. For a read-heavy store, that is a real speed boost over a plain lock.
HashMap is fine here because the lock guards it
A plain HashMap is not thread-safe on its own. It is safe in this example only because every access goes through the read or write lock. The lock is what makes it safe, not the map. If you skip the lock on even one path, the safety is gone.
β¬οΈ Lock downgrading (a brief note)
One neat trick. A thread holding the write lock can grab the read lock before releasing the write lock. This is lock downgrading:
- It goes from the exclusive write lock down to the shared read lock without ever letting go.
- Use it when a thread writes a value, then wants to keep reading it while letting other readers back in.
- No gap appears where another writer could sneak in first.
The example below writes a value, then downgrades to read it safely:
rwLock.writeLock().lock();try { data.put("theme", "light"); // write the new value rwLock.readLock().lock(); // take read lock BEFORE releasing write} finally { rwLock.writeLock().unlock(); // now holding only the read lock}try { System.out.println(data.get("theme")); // read it, others may read too} finally { rwLock.readLock().unlock();}Grab the read lock while you still hold the write lock, then release the write lock. The reverse, read up to write, is not allowed and freezes your thread. Downgrading is fine, upgrading is not.
βοΈ When NOT to use a ReadWriteLock
A ReadWriteLock does more bookkeeping than a plain lock, because it tracks active readers and coordinates them with writers. That cost only pays off when reads heavily outnumber writes:
- Use it when data is read often and written rarely, like a cache, config, or lookup table.
- Skip it when data is written often. The write lock stays exclusive most of the time, so readers rarely share, and you pay overhead for nothing.
- For write-heavy data, a plain
ReentrantLockis simpler and often faster. - For a simple counter, an atomic variable beats both, which is where we head next.
β οΈ Common Mistakes
A few ReadWriteLock traps to avoid.
- Forgetting to unlock. If you do not release a lock, every thread that needs it waits forever. Always unlock in a
finally, so even an exception releases the lock.
// β Wrong: if an exception is thrown, the lock is never releasedrwLock.readLock().lock();String v = data.get(key);rwLock.readLock().unlock();
// β
Right: finally guarantees the unlock runsrwLock.readLock().lock();try { return data.get(key);} finally { rwLock.readLock().unlock();}- Trying to upgrade a read lock to a write lock. Taking the write lock while you already hold the read lock will freeze your thread. The write lock waits for all readers to leave, but you are one of those readers, so you wait for yourself forever.
// β Wrong: holding read, then asking for write -> deadlocks itselfrwLock.readLock().lock();rwLock.writeLock().lock(); // waits for the read lock you are holding
// β
Right: release the read lock first, then take the write lockrwLock.readLock().unlock();rwLock.writeLock().lock();try { data.put(key, value);} finally { rwLock.writeLock().unlock();}-
Using it for write-heavy data. If writes are frequent, the write lock is held most of the time and readers almost never overlap. You get the cost of a ReadWriteLock with none of the benefit. Use a plain lock instead.
-
Unlocking the wrong lock. The read lock and write lock are separate. If you
lock()the read lock you mustunlock()the read lock, not the write lock. Mixing them up leaves a lock held and breaks everything.
β Best Practices
Habits for safe, fast read-write locking.
- Always unlock in a
finallyblock. Pair everylock()with anunlock()that is guaranteed to run. - Use the read lock for reads and the write lock for writes. Do not take the write lock just to read; that throws away the whole benefit.
- Reach for it only when reads greatly outnumber writes. A cache or config is the sweet spot. Write-heavy data should use a plain lock.
- Never try to upgrade read to write. Release the read lock first, then take the write lock, or use downgrading instead.
- Keep the locked section small. Do only the read or write inside the lock, and leave slow or unrelated work outside it.
- Store the lock as a
private finalfield. One shared lock object that nobody outside the class can grab keeps your timing under your control. - Guard every access to the shared data. A non-thread-safe collection like
HashMapis only safe if every path goes through the lock.
π§© What Youβve Learned
Nicely done. Letβs recap the ReadWriteLock.
- β A ReadWriteLock splits one lock into a shared read lock and an exclusive write lock.
- β Many readers can hold the read lock at once, but the write lock is held by one thread alone.
- β This boosts speed for read-heavy data, because readers no longer wait for each other.
- β
Use
readLock().lock()/unlock()andwriteLock().lock()/unlock(), always unlocking in afinallyblock. - β A cache or config read often and written rarely is the ideal use case.
- β Lock downgrading (write to read) is allowed; upgrading (read to write) freezes the thread.
- β
For write-heavy data, a plain
ReentrantLockis simpler and often faster.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
How does a ReadWriteLock differ from a plain lock?
Why: A ReadWriteLock has two locks: a shared read lock many threads can hold, and an exclusive write lock held by one thread.
- 2
How many threads can hold the read lock at the same time?
Why: The read lock is shared, so many readers can hold it together, but not while a writer holds the write lock.
- 3
When is a ReadWriteLock the best choice?
Why: Read-heavy data, like a cache or config, benefits most because readers run in parallel.
- 4
What happens if a thread holding the read lock tries to take the write lock?
Why: Upgrading read to write deadlocks the thread; the write lock waits for the read lock the same thread still holds.
π Whatβs Next?
A ReadWriteLock is powerful, but for a simple counter or flag it is more than you need. Java has lighter tools that make single values safe without any lock, using one unbreakable hardware step. Next you will learn about atomic variables like AtomicInteger, how they work, and when they beat locks. Letβs learn it.