CodeGym /Courses /JAVA 25 SELF /ReentrantLock and ReadWriteLock: differences and examples...

ReentrantLock and ReadWriteLock: differences and examples

JAVA 25 SELF
Level 52 , Lesson 2
Available

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!

1
Task
JAVA 25 SELF, level 52, lesson 2
Locked
Central warehouse inventory tracking 📦
Central warehouse inventory tracking 📦
1
Task
JAVA 25 SELF, level 52, lesson 2
Locked
Global application settings registry ⚙️
Global application settings registry ⚙️
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION