一、什么是读写锁?
读写锁是一种特殊的锁,它允许多个线程同时读 数据,但只允许一个线程写数据。简单来说就是:
读写锁的原则:
- 读读不互斥:多个线程可以同时读数据
- 读写互斥:有线程在读时不能写,有线程在写时不能读
- 写写互斥:同时只能有一个线程写数据
二、为什么需要读写锁?
传统锁的问题:
java
// 使用普通锁时
synchronized void readData() {
// 即使只是读取,也要排队等待
}
synchronized void writeData() {
// 写入数据
}
问题:多个线程只是读取数据,也要互相等待,效率低下。
读写锁的优势:
- 提高性能:对于读多写少的场景,允许多个线程同时读取
- 保证数据一致性:写操作时排他,防止脏读
- 更细粒度的控制:区分读操作和写操作
三、Java中的ReadWriteLock接口
Java提供了java.util.concurrent.locks.ReadWriteLock接口:
java
public interface ReadWriteLock {
Lock readLock(); // 获取读锁
Lock writeLock(); // 获取写锁
}
主要实现类:ReentrantReadWriteLock
java
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private int value;
public void read() {
readLock.lock();
try {
System.out.println("读取数据: " + value);
} finally {
readLock.unlock();
}
}
public void write(int newValue) {
writeLock.lock();
try {
value = newValue;
System.out.println("写入数据: " + newValue);
} finally {
writeLock.unlock();
}
}
}
四、简单易懂的例子
场景:图书馆借书
java
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Library {
// 图书数据
private String bookContent = "这是一本好书...";
// 读写锁
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 读者读书(可以多人同时读)
public void readBook(String readerName) {
lock.readLock().lock(); // 获取读锁
try {
System.out.println(readerName + " 正在读书: " + bookContent.substring(0, 10) + "...");
Thread.sleep(1000); // 模拟阅读时间
System.out.println(readerName + " 读完了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.readLock().unlock(); // 释放读锁
}
}
// 作者修改书(一次只能一个人修改)
public void writeBook(String authorName, String newContent) {
lock.writeLock().lock(); // 获取写锁
try {
System.out.println(authorName + " 开始修改书籍...");
Thread.sleep(2000); // 模拟写作时间
bookContent = newContent;
System.out.println(authorName + " 修改完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock(); // 释放写锁
}
}
}
测试代码:
java
public class TestLibrary {
public static void main(String[] args) {
Library library = new Library();
// 创建多个读者线程
for (int i = 1; i <= 3; i++) {
String readerName = "读者" + i;
new Thread(() -> library.readBook(readerName)).start();
}
// 创建一个作者线程
new Thread(() -> library.writeBook("作者1", "新的内容...")).start();
// 再创建一些读者
for (int i = 4; i <= 5; i++) {
String readerName = "读者" + i;
new Thread(() -> library.readBook(readerName)).start();
}
}
}
五、核心特性详解
1. 锁降级(重要特性)
允许从写锁 降级为读锁,但不能升级。
java
public void lockDowngrade() {
writeLock.lock(); // 先获取写锁
try {
// 修改数据
value = 100;
// 降级为读锁(必须先获取读锁,再释放写锁)
readLock.lock();
} finally {
writeLock.unlock(); // 释放写锁,降级为读锁
}
try {
// 此时仍然持有读锁,可以安全读取
System.out.println("读取: " + value);
} finally {
readLock.unlock();
}
}
2. 公平性选择
java
// 非公平锁(默认,吞吐量高)
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 公平锁(按申请顺序获得锁)
ReentrantReadWriteLock fairLock = new ReentrantReadWriteLock(true);
3. 重入性支持
- 读锁:同一线程可多次获取读锁
- 写锁:同一线程可多次获取写锁
- 写线程可以获取读锁,但读线程不能获取写锁
六、使用注意事项
1. 避免锁饥饿
长时间有读锁时,写线程可能一直等待。
java
// 解决方法:使用公平锁,但性能会下降
ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
2. 注意锁的释放
一定要在finally块中释放锁:
java
readLock.lock();
try {
// 业务代码
} finally {
readLock.unlock(); // 确保锁被释放
}
3. 适用场景
- ✅ 适合:读多写少的场景(如缓存、配置信息)
- ❌ 不适合:写操作频繁的场景
- ❌ 不适合:读取时间非常长的场景(会导致写线程饥饿)
七、与普通锁的对比
| 特性 | 普通锁 (synchronized) | 读写锁 (ReadWriteLock) |
|---|---|---|
| 读读并发 | ❌ 不允许 | ✅ 允许 |
| 读写并发 | ❌ 不允许 | ❌ 不允许 |
| 写写并发 | ❌ 不允许 | ❌ 不允许 |
| 性能 | 简单但效率低 | 复杂但效率高 |
| 适用场景 | 简单同步 | 读多写少 |
八、实际应用示例:缓存实现
java
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Cache<K, V> {
private final Map<K, V> map = new HashMap<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 获取数据(多个线程可同时读)
public V get(K key) {
lock.readLock().lock();
try {
return map.get(key);
} finally {
lock.readLock().unlock();
}
}
// 放入数据(一次只能一个线程写)
public void put(K key, V value) {
lock.writeLock().lock();
try {
map.put(key, value);
} finally {
lock.writeLock().unlock();
}
}
// 清空缓存
public void clear() {
lock.writeLock().lock();
try {
map.clear();
} finally {
lock.writeLock().unlock();
}
}
}
总结
读写锁的核心思想:共享读,独占写
- 优点:在读多写少的场景中显著提高性能
- 缺点:实现复杂,可能引起写线程饥饿
- 关键点 :
- 读锁是共享的,写锁是独占的
- 读写互斥,写写互斥,但读读不互斥
- 支持锁降级,不支持锁升级
- 使用时要确保正确释放锁
建议:如果有一个数据结构,大部分操作是读取,只有少数情况会修改,那么使用读写锁是个好选择!