In Example 23.13, the class ReentrantRWLockCounter implements a thread-safe counter using a ReentrantReadWriteLock. It is a reworking of the counter in Example 23.12 that uses a ReentrantLock. In Example 23.13, the write lock allows mutually exclusive access to write operations and the read lock is shared concurrently by read operations. The numbered comments below correspond to the numbered code lines in Example 23.13.

(1)–(3) A non-fair ReentrantReadWriteLock is created, and its read and write locks obtained.

(4) The read lock acquired by a thread in the getValue() method ensures that the counter can be read safely, as no other thread can hold the write lock to modify the counter. This approach allows concurrent read operations and prevents concurrent write operations on the mutable counter, making it thread-safe. The read lock is guaranteed to be released in the finally block of the try-finally statement.

(5) The increment() method uses the write lock to provide exclusive access to increment the counter.

(6) The incrementAndGet() method shows that it is safe to increment and read the counter when the thread holds the write lock, as no other thread can access the counter. The return statement at (7) is equivalent to the statements at (8) and (9). The call to the getValue() method at (9) acquires the read lock to read the counter value, while the current write thread is still holding the write lock. This illustrates the reentrant nature of the read-write lock, which allows a thread holding the write lock to acquire the read lock.

(10) The getAndIncrement() method shows that it is safe to read and increment the counter when the thread holds the write lock, as no other thread can access the counter.

(12) The incrIfPossible() method illustrates polling for the write lock. It uses the non-blocking tryLock() method in an attempt to acquire the write lock. If successful, it increments the counter, releases the write lock, and returns true. Otherwise, it returns false.

(13) The getIfPossible() method illustrates polling for the read lock. It uses the non-blocking tryLock() method in an attempt to acquire the read lock. If successful, it releases the read lock, and returns an OptionalInt with the value of the counter. Otherwise, it returns an empty OptionalInt, since the read lock could not be acquired.

Note that the methods in Example 23.13 implement atomic actions. When testing the ReentrantRWLockCounter class with Example 23.7, p. 1451, the output shows that the counter was incremented correctly by the threads.

The ReentrantReadWriteLock class also provides miscellaneous methods that can be used to query a read-write lock:

boolean isFair()

Determines whether this lock has its fairness policy set to true.

int getReadHoldCount()
int getWriteHoldCount()

Queries the number of reentrant read holds and reentrant write holds on this lock by the current thread, respectively.

Click here to view code image

boolean isWriteLockedByCurrentThread()

If the write lock is held by the current thread, returns true; otherwise, it returns false.

The fairness policy comes into play when the currently held lock is released and there are threads waiting for a lock. For a read-write lock, there may be both writer threads and reader threads waiting to acquire a lock. The following aspects of a read-write lock’s fairness policy should be noted:

  • In order to assign the read lock, the fairness policy basically chooses between one longest-waiting writer thread among any waiting writer threads to assign the write lock and the longest-waiting group of reader threads among any waiting reader threads. Whichever of these has waited the longest is chosen.
  • A thread can only acquire a fair read lock if both the write lock is free and there are no waiting writer threads. The thread will only acquire the read lock after the oldest currently waiting writer thread has acquired and released the write lock.
  • A thread can only acquire a fair write lock if both the read lock and write lock are free; otherwise, it will block.

Analogous to the tryLock() methods of the ReentrantLock class, the tryLock() methods of the inner classes ReadLock and WriteLock do not honor the fairness policy, acquiring the lock immediately if available, regardless of any waiting threads.

Analogous to the reentrant nature of a ReentrantLock, the read-write lock allows both reader and writer threads to reacquire its read lock or write lock. Note that a thread that holds the write lock can acquire the read lock, but not vice versa. A reader thread that tries to acquire the write lock will block and never succeed.

Although a ReentrantReadWriteLock allows a greater level of concurrency to access shared data than a ReentrantLock, many factors can influence the performance of a read-write lock. Profiling is recommended to gauge the impact of such factors as its premises of more frequent reads than writes, duration of read and write operations, and number of cores available to leverage parallel execution. However, other thread-safe solutions provided by the Concurrency API for accessing shared data should also be considered.