Java Concurrency: ReadWriteLock

Imagine a scenario where we want to distinguish write threads and read threads.

  • As we execute a write we want isolation
  • As we execute a read we don’t want data to change
  • Read operations does not block other read operations

From a writers perspective we want to achieve isolation and consistency. From a readers perspective we want to be able to read results that are consistent, we want to take advantage of the java memory model and we don’t want to prevent other readers from reading too.
The ReadWriteLock can fulfil the above.

A ReadWriteLock maintains a pair of associated locks, one for read-only operations and one for writing. The read lock may be held simultaneously by multiple reader threads, so long as there are no writers. The write lock is exclusive.
All ReadWriteLock implementations must guarantee that the memory synchronization effects of writeLock operations (as specified in the Lock interface) also hold with respect to the associated readLock. That is, a thread successfully acquiring the read lock will see all updates made upon previous release of the write lock.

Let’s examine the interface

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

In both cases we receive back the lock interface which we saw previously.

We have two implementations of a ReadWriteLock the ReentrantReadWriteLock and ReadWriteLockView, we will examine them further.
For now we shall use the ReentrantReadWriteLock implementation.

Let’s see some usage scenarios using Threads and Runables.

We have the write and read lock acquisition scenarios:

    private int counter = 0;

    @Test
    void testWrite() {
        Lock lock = readWriteLock.writeLock();

        lock.lock();

        try {
            counter++;
        } finally {
            lock.unlock();;
        }
    }

    @Test
    void testRead() {
        Lock lock = readWriteLock.readLock();

        lock.lock();

        try {
            log.info("Counter is {}", counter);
        } finally {
            lock.unlock();;
        }
    }

The next scenario is read first and then try to perform a write:

    private int counter = 0;

    @Test
    void testReadThenWrite() throws InterruptedException {
        Thread readThread = new Thread(() -> {
            int readValue;
            Lock lock = readWriteLock.readLock();

            lock.lock();

            try {
                readValue = counter;
                log.info("Read successfully [{}]",readValue);
                sleep();
            } finally {
                lock.unlock();;
            }
        });

        Thread writeThread = new Thread(() -> {
            Lock lock = readWriteLock.writeLock();
            lock.lock();

            try {
                counter++;
                log.info("wrote successfully [{}]",counter);
            } finally {
                lock.unlock();;
            }

        });

        readThread.start();
        Thread.sleep(1000L);
        writeThread.start();
        writeThread.join();
        readThread.join();
    }

    private static void sleep() {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

In this case we can see that the write operation waited for the read operation to finish.

08:40:31.686 [Thread-0] INFO com.gkatzioura.concurrency.lock.readwrite.ReadWriteShowcase - Read successfully [0]
08:40:41.693 [Thread-1] INFO com.gkatzioura.concurrency.lock.readwrite.ReadWriteShowcase - wrote successfully [1]

The other scenario is write first and then try to perform a read:

    private int counter = 0;

    @Test
    void testWriteThenRead() throws InterruptedException {
        Thread readThread = new Thread(() -> {
            int readValue;
            Lock lock = readWriteLock.readLock();

            lock.lock();

            try {
                readValue = counter;
                log.info("Read successfully [{}]",readValue);
            } finally {
                lock.unlock();;
            }
        });

        Thread writeThread = new Thread(() -> {
            Lock lock = readWriteLock.writeLock();
            lock.lock();

            try {
                counter++;
                log.info("wrote successfully [{}]",counter);
                sleep();
            } finally {
                lock.unlock();;
            }

        });

        writeThread.start();
        Thread.sleep(1000L);
        readThread.start();

        writeThread.join();
        readThread.join();
    }

    private static void sleep() {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

As expected the read operation waited for the write operation to finish:

08:42:21.646 [Thread-1] INFO com.gkatzioura.concurrency.lock.readwrite.ReadWriteShowcase - wrote successfully [1]
08:42:31.652 [Thread-0] INFO com.gkatzioura.concurrency.lock.readwrite.ReadWriteShowcase - Read successfully [1]

The last scenario will perform multiple reads. ReadWriteLock has the feature where the read lock can be held by multiple readers.

    @Test
    void testMultipleReads() throws InterruptedException {
        Thread readThread1 = new Thread(() -> {
            int readValue;
            Lock lock = readWriteLock.readLock();

            lock.lock();

            try {
                readValue = counter;
                log.info("Read successfully [{}]",readValue);
                sleep();
            } finally {
                lock.unlock();;
            }
        });
        readThread1.setName("r-1");

        Thread readThread2 = new Thread(() -> {
            int readValue;
            Lock lock = readWriteLock.readLock();

            lock.lock();

            try {
                readValue = counter;
                log.info("Read successfully [{}]",readValue);
            } finally {
                lock.unlock();;
            }

        });
        readThread2.setName("r-2");

        readThread1.start();
        Thread.sleep(1000L);
        readThread2.start();

        readThread1.join();
        readThread2.join();
    }

In both case the response was immediate since there was no blocking at all.

08:46:05.018 [r-1] INFO com.gkatzioura.concurrency.lock.readwrite.ReadWriteShowcase - Read successfully [0]
08:46:06.022 [r-2] INFO com.gkatzioura.concurrency.lock.readwrite.ReadWriteShowcase - Read successfully [0]

From the above scenarios we can get an idea where ReentRantLock can be useful. The predominant scenario would be when we have primarily read operations and get advantage of multiple readers getting a hold on the Read lock. On another blog we will do some benchmarks and see its usage further.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.