Read-write locks introduce a subtle topic that ordinary exclusive locks do not: moving between read and write modes.

In practice:

  • downgrading is supported in a controlled way
  • upgrading is the dangerous direction

Understanding that distinction prevents a lot of broken cache and refresh code.


Problem Statement

A common read-mostly pattern looks like this:

  • read cached data quickly if it exists
  • if missing, update it exclusively
  • continue reading the fresh value

That sounds simple.

The trouble starts when developers try to move from a read lock to a write lock directly and assume the lock will “just upgrade.”

That assumption is where many broken designs begin.


What Is Downgrading

Downgrading means:

  1. hold the write lock
  2. acquire the read lock
  3. release the write lock

That lets a thread:

  • finish a mutation safely
  • continue reading under read protection

This is useful when one thread populates or updates state and then wants to keep reading it without reopening a race window.


What Is Upgrading

Upgrading means:

  1. hold the read lock
  2. try to acquire the write lock without fully leaving read mode first

This is the risky direction because:

  • other readers may still exist
  • several readers may all try to upgrade
  • each one may wait for the others to leave

That can lead to deadlock-like behavior or incorrect assumptions about safety.

For practical Java design, read-to-write upgrading should not be treated as a normal path.


Runnable Example

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LockDowngradingDemo {

    public static void main(String[] args) {
        CachedConfig cache = new CachedConfig();
        System.out.println(cache.getOrRefresh("payments"));
    }

    static final class CachedConfig {
        private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        private final Map<String, String> values = new HashMap<>();

        String getOrRefresh(String key) {
            lock.readLock().lock();
            try {
                String current = values.get(key);
                if (current != null) {
                    return current;
                }
            } finally {
                lock.readLock().unlock();
            }

            lock.writeLock().lock();
            try {
                String current = values.get(key);
                if (current == null) {
                    current = "node-a";
                    values.put(key, current);
                }

                lock.readLock().lock();
                try {
                    return current;
                } finally {
                    lock.writeLock().unlock();
                }
            } finally {
                lock.readLock().unlock();
            }
        }
    }
}

The key order is:

  • acquire write lock
  • acquire read lock
  • release write lock

That is safe downgrading.


Why Upgrade Is Hard

If a thread holds a read lock and tries to move directly to write mode:

  • other readers may still be active
  • each reader may be waiting for the others
  • no one may be able to progress safely

This is why the usual safe pattern is:

  1. release the read lock
  2. acquire the write lock
  3. recheck the state under write protection
  4. perform the mutation if still needed

That recheck step is essential because the state may have changed while you were between modes.


Production-Style Scenario

A cache lookup path often wants to:

  • read fast if data exists
  • escalate to exclusive mutation only when missing

The safe implementation is usually:

  • read under read lock
  • release it if mutation is needed
  • acquire write lock
  • recheck
  • populate
  • optionally downgrade to a read lock if continued read protection is useful

This is a very common pattern in read-mostly systems.


Why This Topic Matters

Read-write locks are already more complex than plain exclusive locks.

If you also misunderstand transition behavior between modes, you can create:

  • latent deadlocks
  • broken assumptions about exclusive access
  • hard-to-reproduce race windows in cache refresh logic

That is why this topic deserves explicit treatment instead of one passing warning.


Common Mistakes

  • assuming read-to-write upgrade is a normal safe operation
  • mutating after releasing the read lock but before reacquiring proper write protection
  • forgetting to recheck state after acquiring the write lock
  • using read-write locks where immutable snapshot replacement would be simpler

Mode transition complexity is often a sign to re-evaluate whether the design needs this lock at all.


Key Takeaways

  • Lock downgrading is supported: write first, then read, then release write.
  • Read-to-write upgrading is the dangerous direction and should not be treated as a normal path.
  • In practice, release read, acquire write, and recheck the state under write protection.
  • Read-write locks are useful only when the workload and discipline justify the added complexity.

Next post: StampedLock in Java Optimistic Read Read Lock and Write Lock

Comments