skip to content

What is a ReadWriteLock and when would you use one instead of a plain lock or synchronized?

level: middleimportance: must knowfreq 62%

answer

  1. Many readers OR one writer, never both
  2. ReentrantReadWriteLock is the implementation
  3. Win only when reads >> writes
  4. Downgrade yes, upgrade deadlocks
  5. Writers can starve in non-fair mode

basics

~20 s

A ReadWriteLock has two locks: a read lock many threads can hold at once, and a write lock only one thread can hold (with no readers at the same time). Use it when data is read far more often than written, so reads don't block each other.

solid answer

~40 s

A ReadWriteLock (an interface, with ReentrantReadWriteLock as the standard implementation) splits one logical lock into a read lock and a write lock. Many threads may hold the read lock at once because concurrent reads of unchanging data are safe; the write lock is exclusive — it excludes all other readers and writers. The win over synchronized or a plain ReentrantLock appears when reads vastly outnumber writes: reads run in parallel instead of serializing on one monitor. You acquire via lock.readLock().lock() and lock.writeLock().lock(), always unlocking in a finally. Trade-offs: more complexity, higher per-acquire overhead than a simple lock, and under heavy write traffic it offers no benefit (writers can even starve under a read-biased policy). ReentrantReadWriteLock supports lock downgrading (hold write, acquire read, release write) but not upgrading, which deadlocks.

go deeper

for a junior

Knows there are two locks: read (shared by many) and write (exclusive to one), and that it suits read-heavy data.

for a middle

Names ReentrantReadWriteLock, uses try/finally, and articulates the many-readers-or-one-writer invariant plus when it beats synchronized.

for a senior

Discusses fairness/writer starvation, the downgrade-yes/upgrade-no rule, per-acquire overhead, and when a plain lock or StampedLock is the better choice.

for a principal

Reasons about contention profiles end-to-end: chooses between synchronized, ReentrantReadWriteLock, and StampedLock based on read/write ratio, section size, reentrancy and condition needs, and measured throughput rather than intuition.

### The problem it solves When multiple threads share **mutable** data (data that can change), you must coordinate access so no thread sees a half-updated, inconsistent state. The simplest tool is a **mutual-exclusion lock** (a *mutex*): `synchronized` or `java.util.concurrent.locks.ReentrantLock`. A mutex lets exactly **one** thread into the protected region (the *critical section*) at a time. That is correct but wasteful when most threads only *read*: two threads reading the same unchanging value cannot interfere, yet a mutex still forces them to take turns. ### The core idea: separate read access from write access A **ReadWriteLock** is an interface in `java.util.concurrent.locks` that hands out **two** related locks via `readLock()` and `writeLock()`: - The **read lock** (a *shared* lock) can be held by **many threads at the same time**, as long as no thread holds the write lock. - The **write lock** (an *exclusive* lock) can be held by **only one** thread, and while it is held **no** thread may hold the read lock. The invariant is: *any number of readers, OR a single writer, but never both at once.* This is safe because concurrent reads of data that nobody is modifying never produce wrong results; only writes need exclusivity. ### The standard implementation The concrete class is `ReentrantReadWriteLock`. "Reentrant" means a thread that already holds a lock can acquire it again without deadlocking (the lock counts how many times the same thread took it and only frees it after the matching number of releases). ```java ReadWriteLock rw = new ReentrantReadWriteLock(); rw.readLock().lock(); // shared try { /* read shared state */ } finally { rw.readLock().unlock(); } rw.writeLock().lock(); // exclusive try { /* mutate shared state */ } finally { rw.writeLock().unlock(); } ``` The `try/finally` is mandatory: explicit locks (unlike `synchronized`) are **not** released automatically when the block exits, so an exception would otherwise leak the lock forever. ### When it actually helps — and when it doesn't A ReadWriteLock pays off only when the workload is **read-heavy**: many concurrent reads, infrequent writes (a cache, a configuration holder, a rarely-changing lookup table). Then readers run in **parallel** instead of serializing on one monitor. It does **not** help — and is often *slower* than a plain lock — when: - Writes are frequent (every write blocks all readers anyway, and you pay extra bookkeeping overhead). - Critical sections are tiny (the lock's own overhead dominates). ### Fairness and writer starvation `ReentrantReadWriteLock` can be constructed *fair* or *non-fair* (the default). In non-fair mode, if readers keep arriving a waiting writer can be **starved** (never gets a turn). Fair mode grants the lock roughly in arrival order, reducing starvation at the cost of throughput. ### Reentrancy details: downgrading vs upgrading - **Downgrading** is allowed: a thread holding the *write* lock may acquire the *read* lock, then release the write lock — staying continuously protected while narrowing to shared access. - **Upgrading** is **not** allowed: a thread holding the *read* lock that tries to acquire the *write* lock will **deadlock** (the write acquire waits for all readers — including itself — to release). You must release the read lock first. ### Relationship to StampedLock `StampedLock` (Java 8+) targets the same read-heavy scenario but adds an **optimistic read** mode that takes *no* lock at all for reads, validating afterward. It is faster under low contention but is **not reentrant** and not a drop-in `ReadWriteLock`. ReentrantReadWriteLock remains the choice when you need reentrancy, condition variables, or lock downgrading.

  • Why does lock upgrading deadlock but downgrading works?
    Upgrading: a thread holding the read lock requests the write lock, which must wait for all readers to release — including that same thread, which is waiting on itself. Downgrading works because acquiring the (shared) read lock while already holding the exclusive write lock is always grantable; you then release the write lock.
  • What is writer starvation and how do you address it?
    Under a non-fair read-biased policy, a continuous stream of readers can keep the write lock perpetually unavailable. Constructing ReentrantReadWriteLock in fair mode grants access roughly in arrival order so a waiting writer eventually proceeds, at some throughput cost.

saying these in an interview costs you the question

  • Claiming readers and writers can hold the lock simultaneously
  • Saying a ReadWriteLock is always faster than synchronized (it loses under write-heavy or tiny critical sections)
  • Forgetting the try/finally unlock — explicit locks don't auto-release
  • Thinking you can upgrade from read lock to write lock (it deadlocks)

context