Class 35: ReadWriteLock and Read-Write Operations
Part 6 — Locks and Concurrent Collections
In the previous class, we learned about ReentrantLock. It allows us to explicitly acquire and release locks while protecting shared data.
But let's consider a different situation.
Imagine that an application has a shared configuration object. Many threads need to read its values, but only a few threads occasionally update them.
Should every reader have to wait for other readers to finish?
Not necessarily. Reading data does not usually change it. Multiple threads can safely read the same data at the same time, provided no thread is modifying it.
Java provides a locking mechanism designed for this situation: ReadWriteLock.
1. Understanding the Problem
Let's first understand a simple example.
Imagine an online shopping application where thousands of users can view product details.
Occasionally, an administrator updates a product's price.
We have two types of operations:
- Read operation: Viewing product details.
- Write operation: Updating product details.
Multiple users should be able to read the product details simultaneously.
However, when someone updates the product details, other readers and writers should wait until the update is complete.
This is exactly the kind of situation where a ReadWriteLock can be useful.
2. What Is ReadWriteLock?
ReadWriteLock is an interface available in the
java.util.concurrent.locks package.
It provides two separate locks:
- Read Lock: Allows multiple threads to read shared data simultaneously.
- Write Lock: Allows only one thread to modify the shared data at a time, while preventing other readers and writers from accessing it.
Let's understand the rules.
| Situation | Allowed? |
|---|---|
| Multiple threads reading together | Yes |
| One thread writing while another reads | No |
| Multiple threads writing together | No |
| One thread reading while another reads | Yes |
The important idea is that reading can happen concurrently, but writing requires exclusive access.
3. Creating a ReadWriteLock
Java provides an implementation called
ReentrantReadWriteLock.
We can create it like this:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
ReadWriteLock lock = new ReentrantReadWriteLock();
Notice that we are declaring the variable using the ReadWriteLock interface and creating an object of ReentrantReadWriteLock.
This is similar to declaring a List reference and creating an ArrayList object.
Now we can access the two locks:
lock.readLock();
lock.writeLock();
The first gives us the read lock, and the second gives us the write lock.
4. Using the Read Lock
Let's start with a simple example.
We will create a shared resource containing a value. Multiple threads will read that value.
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class SharedData {
private int value = 100;
private final ReadWriteLock lock =
new ReentrantReadWriteLock();
public int readValue() {
lock.readLock().lock();
try {
System.out.println(
Thread.currentThread().getName() + " is reading"
);
return value;
} finally {
lock.readLock().unlock();
}
}
}
public class ReadLockExample {
public static void main(String[] args) {
SharedData data = new SharedData();
Runnable task = () -> {
int result = data.readValue();
System.out.println(
Thread.currentThread().getName()
+ " read value: " + result
);
};
Thread thread1 = new Thread(task, "Thread 1");
Thread thread2 = new Thread(task, "Thread 2");
Thread thread3 = new Thread(task, "Thread 3");
thread1.start();
thread2.start();
thread3.start();
}
}
All three threads use the same SharedData object.
Each thread acquires the read lock before accessing the shared value.
Because multiple threads can hold the read lock together, they do not need to wait for one another.
The exact order of the output may vary.
5. Using the Write Lock
Now let's see how we update the shared data.
We will add a method that changes the value.
public void updateValue(int newValue) {
lock.writeLock().lock();
try {
System.out.println(
Thread.currentThread().getName() + " is updating"
);
value = newValue;
} finally {
lock.writeLock().unlock();
}
}
Notice that we are using:
lock.writeLock().lock();
A thread must acquire the write lock before modifying the shared data.
While a thread holds the write lock:
- Other threads cannot acquire the read lock.
- Other threads cannot acquire the write lock.
Once the update is complete, the thread releases the write lock.
6. Complete Example: Reading and Updating Shared Data
Let's combine both operations into one practical example.
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class SharedData {
private int value = 100;
private final ReadWriteLock lock =
new ReentrantReadWriteLock();
public int readValue() {
lock.readLock().lock();
try {
System.out.println(
Thread.currentThread().getName() + " is reading"
);
return value;
} finally {
lock.readLock().unlock();
}
}
public void updateValue(int newValue) {
lock.writeLock().lock();
try {
System.out.println(
Thread.currentThread().getName() + " is updating"
);
value = newValue;
System.out.println("Updated value: " + value);
} finally {
lock.writeLock().unlock();
}
}
}
public class ReadWriteLockExample {
public static void main(String[] args) {
SharedData data = new SharedData();
Thread reader1 = new Thread(() -> {
System.out.println("Reader 1: " + data.readValue());
});
Thread reader2 = new Thread(() -> {
System.out.println("Reader 2: " + data.readValue());
});
Thread writer = new Thread(() -> {
data.updateValue(500);
});
reader1.start();
reader2.start();
writer.start();
}
}
All three threads share the same SharedData object.
The two readers can read at the same time. The writer needs exclusive access before updating the value.
Depending on which thread acquires its lock first, the writer may update before or after the readers.
What matters is that the locks prevent a reader from accessing the data while a writer is modifying it.
7. Why Do We Need finally Here Too?
Just like ReentrantLock, ReentrantReadWriteLock requires us to release locks explicitly.
Consider this code:
lock.writeLock().lock();
try {
value = 500;
} finally {
lock.writeLock().unlock();
}
If an exception occurs while updating the value, the finally block still executes.
This ensures that the write lock is released.
Always follow this pattern when using explicit locks.
8. ReadWriteLock vs. ReentrantLock
Let's compare the two locking mechanisms.
| Feature | ReentrantLock | ReadWriteLock |
|---|---|---|
| Lock types | One exclusive lock | Separate read and write locks |
| Multiple readers | Cannot hold the same lock together | Can read together |
| Writing | Exclusive access | Exclusive access |
| Useful when | Access needs general protection | Reads are frequent and writes are less frequent |
If most operations only read shared data and updates happen occasionally, ReadWriteLock may allow better concurrency.
However, it is not automatically faster in every situation. The benefit depends on the workload and the cost of acquiring and managing locks.
9. When Should We Use ReadWriteLock?
Let's consider a few practical examples.
- A configuration service where many threads read settings and occasional updates happen.
- A cache where many requests read existing values and some operations update them.
- A shared lookup table that is read frequently and modified less often.
The key idea is to use ReadWriteLock when concurrent reading is useful and updates need exclusive access.
If a resource is updated very frequently, or the protected operations are very small, a simple lock may be a better choice.
Test Your Understanding
Let's see how well you understood this class!
Question 1: What does ReadWriteLock provide?
Question 2: Can multiple threads hold the read lock at the same time?
Question 3: What happens while a thread holds the write lock?
Question 4: Which implementation commonly provides ReadWriteLock in Java?
Question 5: In which situation might ReadWriteLock be useful?
Class Summary
Let's quickly revise what we learned today.
- ReadWriteLock provides separate read and write locks.
- Multiple threads can hold the read lock simultaneously.
- The write lock provides exclusive access.
- ReentrantReadWriteLock is a standard implementation.
- Use readLock() to acquire the read lock.
- Use writeLock() to acquire the write lock.
- Always release acquired locks in a finally block.
- ReadWriteLock can be useful when reading is frequent and writing is less frequent.
Remember: Multiple readers can work together, but a writer needs exclusive access to the shared resource.
You have completed 35 out of 46 classes.