1. Overview

ReadWriteLock offers a greater level of concurrency compared to Lock. It allows multiple threads to read concurrently, while the write operations remain mutually exclusive. In this tutorial, we'll examine the usage of ReentrantReadWriteLock which is an implementation of ReadWriteLock.

2. ReentrantReadWriteLock Usage

The ReadWriteLock interface contains two methods: readLock and writeLock. For the read operations, we must use the Lock instance returned from the readLock method. This way, multiple readers can acquire the read lock and operate concurrently.

public class ReadWriteLockDetails {

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final Lock readLock = readWriteLock.readLock();

    public void acquireReadLock() {
        readLock.lock();
        try {
            doRead();
        } finally {
            readLock.unlock();
        }
    }

    private void doRead() {
        System.out.println("Reading.");
    }
}

Here, we're creating an instance of ReentrantReadWriteLock. Then we're invoking ReadWriteLock.readLock and storing the returned Lock in an instance variable. This is perfectly fine since successive calls to readLock return the same Lock instance.

Secondly, to provide synchronization for write operations, we must use the lock returned from writeLock. When a thread acquires the write lock, all other threads - reading or writing - must wait:

private final Lock writeLock = readWriteLock.writeLock();
 
public void acquireWriteLock() {
    writeLock.lock();
    try {
        doWrite();
    } finally {
        writeLock.unlock();
    }
}

Here, similar to the previous example, we're storing the write lock in an instance variable. Then we're guarding the write operation with this lock.

3. Lock Downgrade

Now, we'll see how we can downgrade from the write lock to a read lock. In essence, when a task acquires the write lock, we'll also acquire the read lock and then release the write lock:

public void downgradeWriteLock() {
    writeLock.lock();
    try {
        doWrite();
        readLock.lock();
    } finally {
        writeLock.unlock();
    }

    try {
        doRead();
    } finally {
        readLock.unlock();
    }
}

With a downgrade, we're reducing the lock wait time for other threads, making the application more responsive.

4. Sample Application

Lastly, we'll see a sample application that makes use of ReadWriteLock. We'll create a course class that allows students to register themselves and query whether someone is registered:

public class ReadOptimizedCourse {

    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    private final Set registeredNames = new HashSet<>();

    public void register(String item) {
        writeLock.lock();
        try {
            registeredNames.add(item);
        } finally {
            writeLock.unlock();
        }
    }

    public boolean isRegistered(String name) {
        readLock.lock();
        try {
            return registeredNames.contains(name);
        } finally {
            readLock.unlock();
        }
    }
}

Here, we're storing the registered names in a HashSet - registeredNames. Remember that HashSet isn't thread-safe, so we're providing external locking via ReentrantReadWriteLock. The register method modifies the set so it uses the write lock to gain exclusive access. On the other hand, isRegistered only reads the set, thus it tries to acquire a read lock allowing multiple threads to query the set.

5. Summary

In this tutorial, we've looked at how we can use the ReentrantReadWriteLock which is an implementation of ReadWriteLock.

As always the source code for all examples is available on Github.