Typically, a read-write tandem (for example, read-write a file) should be accomplished based on two statements:
-
Readers can read simultaneously as long as there are no writers (shared pessimistic lock).
-
A single writer can write at a time (exclusive/pessimistic locking).
The following diagram depicts readers on the left-hand side and writers on the right-hand side:
Mainly, the following behavior is implemented by ReentrantReadWriteLock:
-
Provides pessimistic locking semantics for both locks (read and write lock).
-
If some readers hold the read lock and a writer wants the write lock, then no more readers are allowed to acquire the read lock until the writer released the write lock.
-
A writer can acquire the read lock, but a reader cannot acquire the write lock.
The idiom for using ReentrantReadWriteLock is shown as follows:
ReadWriteLock / ReentrantReadWriteLock lock
= new ReentrantReadWriteLock();
...
lock.readLock() / writeLock().lock();
try {
...
} finally {
lock.readLock() / writeLock().unlock();
}
The following code represents a ReentrantReadWriteLock usage case that reads and writes an integer amount variable:
public class ReadWriteWithLock {
private static final Logger logger
= Logger.getLogger(ReadWriteWithLock.class.getName());
private static final Random rnd = new Random();
private static final ReentrantReadWriteLock lock
= new ReentrantReadWriteLock(true);
private static final Reader reader = new Reader();
private static final Writer writer = new Writer();
private static int amount;
private static class Reader implements Runnable {
@Override
public void run() {
if (lock.isWriteLocked()) {
logger.warning(() -> Thread.currentThread().getName()
+ " reports that the lock is hold by a writer ...");
}
lock.readLock().lock();
try {
logger.info(() -> "Read amount: " + amount
+ " by " + Thread.currentThread().getName());
} finally {
lock.readLock().unlock();
}
}
}
private static class Writer implements Runnable {
@Override
public void run() {
lock.writeLock().lock();
try {
Thread.sleep(rnd.nextInt(2000));
logger.info(() -> "Increase amount with 10 by "
+ Thread.currentThread().getName());
amount += 10;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
logger.severe(() -> "Exception: " + ex);
} finally {
lock.writeLock().unlock();
}
}
...
}
And, let's perform 10 reads and 10 writes with two readers and four writers:
ExecutorService readerService = Executors.newFixedThreadPool(2);
ExecutorService writerService = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
readerService.execute(reader);
writerService.execute(writer);
}
A possible output will be as follows:
[09:09:25] [INFO] Read amount: 0 by pool-1-thread-1
[09:09:25] [INFO] Read amount: 0 by pool-1-thread-2
[09:09:26] [INFO] Increase amount with 10 by pool-2-thread-1
[09:09:27] [INFO] Increase amount with 10 by pool-2-thread-2
[09:09:28] [INFO] Increase amount with 10 by pool-2-thread-4
[09:09:29] [INFO] Increase amount with 10 by pool-2-thread-3
[09:09:29] [INFO] Read amount: 40 by pool-1-thread-2
[09:09:29] [INFO] Read amount: 40 by pool-1-thread-1
[09:09:31] [INFO] Increase amount with 10 by pool-2-thread-1
...