Java 中的读写锁(ReadWriteLock)深入讲解
ReadWriteLock
是 Java 提供的一种特殊锁,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源,且写操作和读操作是互斥的。其主要目的是在读多写少的场景中提高系统的并发性。
Java 中的 ReentrantReadWriteLock
是 ReadWriteLock
的一个常见实现,提供了更细粒度的控制,允许更高的并发性,尤其在大量读操作而写操作较少的场景中。
一、读写锁的基本概念
- 读锁(共享锁):多个线程可以同时持有读锁,只要没有任何线程持有写锁。
- 写锁(排他锁):一次只有一个线程可以持有写锁,同时写锁与读锁是互斥的,即当一个线程持有写锁时,其他线程不能获取读锁或写锁。
二、ReadWriteLock
及其实现
1. ReadWriteLock
接口
ReadWriteLock
是 Java 并发包 java.util.concurrent.locks
中的一个接口。它定义了两个方法,用于获取读锁和写锁:
javaCopy codepublic interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
2. ReentrantReadWriteLock
类
ReentrantReadWriteLock
是 Java 中 ReadWriteLock
的具体实现类。它支持公平锁和非公平锁模式,默认是非公平锁模式。
javaCopy codepublic class ReentrantReadWriteLock implements ReadWriteLock {
public Lock readLock() { // 获取读锁
// 实现代码
}
public Lock writeLock() { // 获取写锁
// 实现代码
}
}
2.1 公平锁与非公平锁
- 公平锁:线程获取锁的顺序是按照它们请求的顺序,先请求的线程优先获取锁,避免线程饥饿。
- 非公平锁:线程可以抢占锁,不按请求的顺序,可能会导致线程饥饿,但通常性能较好。
2.2 重要方法
ReentrantReadWriteLock
提供了两个内部类 ReadLock
和 WriteLock
,分别实现了读锁和写锁。
readLock.lock()
:获取读锁,如果有其他线程持有写锁,则阻塞。writeLock.lock()
:获取写锁,如果有其他线程持有读锁或写锁,则阻塞。
三、ReentrantReadWriteLock
的实现原理
1. 锁的内部状态
ReentrantReadWriteLock
通过维护一个整型变量 state 来存储锁的状态:
- 低 16 位 表示获取到的写锁次数。
- 高 16 位 表示获取到的读锁次数。
2. 读写锁的互斥与共享
- 写锁(独占锁):获取写锁时,必须保证当前没有其他线程持有读锁或写锁。
- 读锁(共享锁):多个线程可以同时持有读锁,但如果有线程持有写锁,则读锁阻塞。
3. AQS (AbstractQueuedSynchronizer)
ReentrantReadWriteLock
是基于 AQS(抽象队列同步器)实现的。AQS 是 Java 中实现锁的核心类,它维护了一个 FIFO 等待队列来管理线程的排队和唤醒。
- 写锁:AQS 中维护的 state 值必须为 0,线程才能获取写锁。如果 state != 0,线程将被挂起,直到锁被释放。
- 读锁:高 16 位的 state 表示读锁的持有次数。获取读锁时,只要没有线程持有写锁,读锁的持有次数就会加 1。
四、读写锁的使用场景
ReadWriteLock
最适合于 读多写少 的场景,即读操作远多于写操作时,通过允许多个线程同时读取,能显著提升系统的并发性。如果写操作较多,读写锁反而可能会导致性能下降,因为读写是互斥的。
常见应用场景
- 缓存系统:多个线程同时读取缓存中的数据,而写操作较少。例如,当缓存命中率较高时,读写锁可以提升并发性。
- 配置文件读取:多个线程可以同时读取配置文件,而只有在管理员修改配置文件时才需要进行写操作。
- 数据分析:多个线程可以同时读取统计数据并进行分析,而只有少数情况需要更新统计数据。
五、读写锁的代码示例
javaCopy codeimport java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private int value = 0;
// 读操作(可以被多个线程同时执行)
public int read() {
rwLock.readLock().lock(); // 获取读锁
try {
System.out.println(Thread.currentThread().getName() + " reading: " + value);
return value;
} finally {
rwLock.readLock().unlock(); // 释放读锁
}
}
// 写操作(同时只能有一个线程执行)
public void write(int newValue) {
rwLock.writeLock().lock(); // 获取写锁
try {
System.out.println(Thread.currentThread().getName() + " writing: " + newValue);
this.value = newValue;
} finally {
rwLock.writeLock().unlock(); // 释放写锁
}
}
public static void main(String[] args) throws InterruptedException {
ReadWriteLockExample example = new ReadWriteLockExample();
// 启动5个读线程
for (int i = 0; i < 5; i++) {
new Thread(() -> example.read()).start();
}
// 启动一个写线程
new Thread(() -> example.write(10)).start();
}
}
解释:
- 在这个示例中,5 个读线程可以同时获取读锁并读取
value
,而只有一个写线程可以修改value
。写操作会阻塞其他读写操作,直到写操作完成。
六、公平锁和非公平锁的实现原理与区别
1. 公平锁
- 实现原理:公平锁是通过 FIFO(先进先出)队列来保证线程获取锁的顺序,即按照请求顺序,先来的线程会先获得锁,后来的线程排队等待。这避免了“线程饥饿”的问题。
- 适用场景:公平锁适用于对锁争用较少且需要保证请求顺序的场景。
2. 非公平锁
- 实现原理:非公平锁允许线程“插队”,即即使有其他线程在等待锁,也允许某个新到的线程直接获取锁。这种方式可能会导致某些线程一直无法获取锁,出现“线程饥饿”的问题,但它通常具有更高的性能。
- 适用场景:非公平锁适用于对锁争用激烈且追求性能优先的场景。
在 ReentrantReadWriteLock
中,默认的锁是 非公平锁,但可以通过构造函数指定为公平锁。
javaCopy code// 默认使用非公平锁
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 使用公平锁
ReentrantReadWriteLock fairLock = new ReentrantReadWriteLock(true);
七、读写锁的优缺点
优点:
- 提高读操作的并发性:允许多个读线程同时读取共享资源,从而提升了系统的吞吐量。
- 适合读多写少场景:在读操作远多于写操作的情况下,性能提升明显。
缺点:
- 写锁会阻塞读锁:如果写操作频繁,读写锁反而会降低性能,因为写锁会阻塞所有的读操作。
- 复杂性增加:相较于普通的锁,读写锁的实现和使用都更加复杂。
总结
ReadWriteLock
是一种读写分离的锁机制,适合于读多写少的场景。它通过ReentrantReadWriteLock
实现,读锁是共享的,写锁是排他的。- 通过 AQS 实现的底层机制,它可以有效管理锁的状态,允许高并发的读操作,并限制写操作。
ReentrantReadWriteLock
支持公平锁和非公平锁,非公平锁性能更好,但可能会导致线程饥饿。- 读写锁的使用需要根据具体场景进行选择,特别是在读写操作频率不均衡的系统中。