Skip to main content
Blog

Readers-writer lock using java 5’s condition variables

By 5 februari 2019februari 7th, 2019No Comments

When I explained a piece of code to a colleague during a code review, I realized I had inadvertently implemented a readers-writer lock. So, I threw the code out, replacing it by the ReadWriteLock from java. This article is about that thrown out code. I updated the method names and comments to reflect the readers-writer lock as opposed to the local business domain.

First, we will look at condition variables. Secondly, we will implement a readers-writer lock using those condition variables. Lastly we will unit test the lock using Awaitility.

Condition variables

Flow-chart of conditions variables

Fig 1. Flow chart of condition variables

Condition variables allow a thread to block while waiting for a condition to change. To do so, a thread acquires the lock before checking the condition, then either conditions are met and the thread proceeds while holding the lock. Or, when the conditions are not met, the thread will block and the lock will be released immediately. Then, when another thread makes a change that might be of interest, it will signal that a change was made, allowing the original thread to wake up, reacquire its lock and recheck the condition. See Fig 1 for a graph of the operation.

A key insight here, is that condition variables are not variables, nor do they ‘do’ anything with a condition. Instead, a condition variable allows to signal that a change has occurred, which will wake up a thread awaiting the same condition variable.

Fig 3. Locked for writing

Fig 2. Locked for reading

Readers-writer lock

A readers-writer lock is a solution for a common problem. When writing to a shared resource, no other thread should write to or read from that resource. But when reading from a resource, we only require that no other thread is writing. Therefore, we can have either one writer, or multiple readers, hence the name readers-writer lock.

Implementation

In the implementation we keep track of the number of active readers. This is not an atomic integer, nor is it marked as volatile, because access to it is guarded by a lock. The field contains either the number of readers, or it marks a write lock, using the value -1. So if activeReaders > 0 then the lock is locked for reading, and when activeReaders = -1 it is locked for writing.

private final Lock lock = new ReentrantLock();
private final Condition onChange = lock.newCondition();
private int activeReaders = 0;

onChange is the condition variable. It is created from the lock.

    public void releaseWriteLock() {
        lock.lock();
        if (activeReaders != -1) {
            throw new ReleasingInvalidLockException(
                    "Unable to release writer lock, not locked for writing.");
        }
        activeReaders = 0;
        // Signal to threads that have called await() to wake up and re-check
        // their condition.
        onChange.signal();
        lock.unlock();
    }
    public void acquireReadLock() {
        lock.lock();
        try {
            while (activeReaders < 0) {
                // Temporarily release the lock, until we can continue.
                try {
                    onChange.await();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    Thread.interrupted();
                }
            }
            activeReaders++;
        } finally {
            lock.unlock();
        }
    }

When acquireReadLock() calls onChange.await(), it will block and suspend that thread. When releaseWriteLock() calls onChange.signal(), the other thread will be woken up, so it can recheck the number of active readers.

There are 2 more similar methods, acquireWriteLock() and releaseReadLock().

Test using Awaitility

The Awaitility library provides methods to test asynchronous method calls. Asynchronous calls will run in another thread, if we check the result too soon, our tests will fail. So we must loop until either our patience runs out, or the assertion is valid. Fortunately, Awaitility does this for us, its API allows us to specify not just what we want to test, but how long we are willing to wait for it.

    @Test
    public void checkReadLockIsBlockedWriteLock() {
        // Given an acquired write lock.
        final ReadersWriterLock lock = new ReadersWriterLock();
        lock.acquireWriteLock();

        // When we subsequently call acquire a read lock.
        final FutureTask<Void> task = new FutureTask<>(() -> {
            lock.acquireReadLock();
            return null;
        });
        new Thread(task).start();
        
        // Then we expect the acquire read lock call to block.
        Awaitility.await().pollDelay(Duration.ONE_MILLISECOND)
                .until(() -> !task.isDone());
        
        // Until the write lock is released.
        lock.releaseWriteLock();
        Awaitility.await().atMost(Duration.ONE_SECOND).until(task::isDone);
    }

In the last line, Awaitility is used to wait until the task is done. The task should finish shortly after lock.releaseWriteLock(), but not immediately. Therefore, testing that it finished immediately would fail. So,  Awaitility polls until the assertion succeeds, for at most a second.

Full source code

https://github.com/First8/readers-writer-lock-and-awaitility

Background information

https://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/locks/ReentrantLock.html

https://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/locks/Condition.html

https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock

https://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/locks/ReadWriteLock.html