I was confused when I first learned about Java ReadWriteLock. It looked straightforward enough that we allow concurrent read-operation but write must be exclusive. I thought write-operation must prepared or save result in ready-to-be-read format so the read-operation could just, well.. , read the data.
If that was the case then why didn’t we use the similar approach of copy-on-write that each write made a copy of data, modify it then save the new result to volatile reference for later read. Wouldn’t that be simpler? It was clear to me later that the name was misleading. ReadLock wasn’t just for read.
Netty HashedWheelTimer
Here is the real-world use in HashedWheelTimer. The class is for scheduling a logic to be run after a timeout. Note that I have simplify the code a bit for readability.
public static void main(String[] args) { HashedWheelTimer timer = new HashedWheelTimer(); timer.newTimeout((timeout)-> System.out.println("run after timeout"),10, TimeUnit.MILLISECONDS); }
The timer.newTimeout()
will add the task to internal data structure which guarded by ReadLock. The modification here could be called concurrently since the underlying data structure is thread-safe; ConcurrentIdentityHashMap.
class DemoHashedWheelTimer{ final int ticksPerWheel = 10; final ReadWriteLock lock = new ReentrantReadWriteLock(); final Set<HashedWheelTimeout>[] wheel = createWheel(ticksPerWheel); private Set<HashedWheelTimeout>[] createWheel(int ticksPerWheel) { //Create array of ConcurrentIdentityHashMap Set return new Set[ticksPerWheel]; } public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) { int stopIndex = computeWheelIndex(); HashedWheelTimeout timeout = createTimeout(task, delay, unit); //Guarded by readLock lock.readLock().lock(); try { //Notice here that logic has made modification on data structure. wheel[stopIndex].add(timeout); } finally { lock.readLock().unlock(); } return timeout; } }
The write-operation in this code is when the background thread wakes up and iterate over the stored timeout to see which one has already expired and the associated tasks must be executed.
The wheel-timer mechanism need to prevent any other change in data structure during the iteration. So the logic is guarded by WriteLock.
void fetchExpiredTimeouts(List<HashedWheelTimeout> expiredTimeouts, long deadline) { // Find the expired timeouts and decrease the round counter if necessary. lock.writeLock().lock(); try { int newWheelCursor = computeNewCursor(); ReusableIterator<HashedWheelTimeout> iterator = iterators[newWheelCursor]; //iterate over the stored timeouts to see which are already expired } finally { lock.writeLock().unlock(); } }
After seeing the code above, it became clear to me that I misunderstand the ReadWriteLock. Read and write are not associated with actual reading data or making modification. We must think of it as operations that run concurrently and operation that run exclusively.
Read-operation is the logic that we allow to run concurrently. It could be just data reading or it also make some modification as long as multiple reads could happen safely at the same time. Write-operation is exclusive It required to be the only thing that is running. All read and other write must wait.