ReentrantLock是一个独占锁,独占的意思是同一时间只允许一个线程访问共享资源,在某些场景中独占锁的性能并不是最好的,比如对共享资源读的时候允许多个线程同时读取,以最大化的提高系统性能,所以又推出了读写锁,本文就来学习读写锁的相关知识。
认识ReadWriteLock
/**
* A {@code ReadWriteLock} maintains a pair of associated {@link
* Lock locks}, one for read-only operations and one for writing.
* The {@linkplain #readLock read lock} may be held simultaneously
* by multiple reader threads, so long as there are no writers.
* The {@linkplain #writeLock write lock} is exclusive.
*/
ReadWriteLock(读写锁)维护一对关联的Lock 锁,一个用于只读操作,另一个用于写入操作。只要没有写入线程,readLock 锁可被多个读线程同时持有;而writeLock 写锁是独占的。
/* <p>All {@code ReadWriteLock} implementations must guarantee that
* the memory synchronization effects of {@code writeLock} operations
* (as specified in the {@link Lock} interface) also hold with respect
* to the associated {@code readLock}. That is, a thread successfully
* acquiring the read lock will see all updates made upon previous
* release of the write lock.
*/
所有ReadWriteLock实现必须确保:writeLock操作的内存同步效应(依据Lock接口规范)同样适用于关联的readLock。即成功获取读锁的线程,能看到之前写锁释放时所做的所有更新。
/* <p>A read-write lock allows for a greater level of concurrency in
* accessing shared data than that permitted by a mutual exclusion lock.
* It exploits the fact that while only a single thread at a time (a
* <em>writer</em> thread) can modify the shared data, in many cases any
* number of threads can concurrently read the data (hence <em>reader</em>
* threads).
* In theory, the increase in concurrency permitted by the use of a read-write
* lock will lead to performance improvements over the use of a mutual
* exclusion lock. In practice this increase in concurrency will only be fully
* realized on a multi-processor, and then only if the access patterns for
* the shared data are suitable.
*/
与互斥锁相比,读写锁在访问共享数据时允许更高程度的并发能力。其核心原理在于:虽然同一时间仅允许单个线程(写线程)修改共享数据,但在多数场景下,任意数量的线程可并发读取数据(即读线程)。
什么情况适合用ReadWriteLock
理论上,使用读写锁带来的并发性提升将优于互斥锁的性能表现;而实际应用中,这种并发性提升只有在多处理器环境下才能充分体现,且前提是共享数据的访问模式符合适用条件。
/* <p>Whether or not a read-write lock will improve performance over the use
* of a mutual exclusion lock depends on the frequency that the data is
* read compared to being modified, the duration of the read and write
* operations, and the contention for the data - that is, the number of
* threads that will try to read or write the data at the same time.
* For example, a collection that is initially populated with data and
* thereafter infrequently modified, while being frequently searched
* (such as a directory of some kind) is an ideal candidate for the use of
* a read-write lock. However, if updates become frequent then the data
* spends most of its time being exclusively locked and there is little, if any
* increase in concurrency. Further, if the read operations are too short
* the overhead of the read-write lock implementation (which is inherently
* more complex than a mutual exclusion lock) can dominate the execution
* cost, particularly as many read-write lock implementations still serialize
* all threads through a small section of code. Ultimately, only profiling
* and measurement will establish whether the use of a read-write lock is
* suitable for your application.
*/
**相较于互斥锁,读写锁能否提升性能取决于三个因素:数据读取与修改的频率比、读写操作的持续时间、以及数据争用强度——即并发尝试读写数据的线程数量。**例如:一个初始化后频繁查询但极少修改的数据集合(如某类目录系统),就是读写锁的理想应用场景。但若更新操作变得频繁,数据将长期处于独占锁定状态,此时并发性提升微乎其微。更进一步,若读操作耗时过短,读写锁实现的开销(其固有复杂度高于互斥锁)可能反超执行成本,尤其因多数读写锁实现仍通过小段代码串行化所有线程。最终,唯有通过性能剖析和数据测量才能判定读写锁是否适用于当前应用。
/* <p>Although the basic operation of a read-write lock is straight-forward,
* there are many policy decisions that an implementation must make, which
* may affect the effectiveness of the read-write lock in a given application.
* Examples of these policies include:
* <ul>
* <li>Determining whether to grant the read lock or the write lock, when
* both readers and writers are waiting, at the time that a writer releases
* the write lock. Writer preference is common, as writes are expected to be
* short and infrequent. Reader preference is less common as it can lead to
* lengthy delays for a write if the readers are frequent and long-lived as
* expected. Fair, or "in-order" implementations are also possible.
*
* <li>Determining whether readers that request the read lock while a
* reader is active and a writer is waiting, are granted the read lock.
* Preference to the reader can delay the writer indefinitely, while
* preference to the writer can reduce the potential for concurrency.
*
* <li>Determining whether the locks are reentrant: can a thread with the
* write lock reacquire it? Can it acquire a read lock while holding the
* write lock? Is the read lock itself reentrant?
*
* <li>Can the write lock be downgraded to a read lock without allowing
* an intervening writer? Can a read lock be upgraded to a write lock,
* in preference to other waiting readers or writers?
*
* </ul>
*/
尽管读写锁基础操作简单直接,但实现时需制定多项策略决策,这些策略将直接影响读写锁在特定场景中的效能。典型策略包括:
1、当读写线程同时在写锁释放时等待,决策授予读锁还是写锁:
(1)写者优先(常见):因预期写操作耗时短且频率低
(2)读者优先(较少用):当读操作如预期般频繁且持久时,可能导致写操作长时间延迟
(3)公平排序:也可采用"先进先出"的实现策略
2、当存在活跃读线程且写线程等待时,决定是否授予新请求的读锁:读者优先策略可能无限期延迟写者执行,而写者优先策略则可能削弱并发潜力。
3、判定锁的可重入性:持有写锁的线程能否重入该写锁?持有写锁时能否再获取读锁?读锁本身是否可重入?
4、写锁能否在不允许其他写者介入的情况下降级为读锁?读锁能否优先于其他等待的读写线程升级为写锁?
综上所属,官方对于使用读写锁是否能带来并发性上的提升是不确定的,受很多因素的制约,所以在评估ReadWriteLock方案是否适用于您的应用时,应全面考量上述所有因素。
ReadWriteLock API
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
jdk里面的接口的定义都非常简约(单一职责原则),ReadWriteLock里面只声明了两个获取读锁和写锁的方法。readLock()方法返回读锁,writeLock()方法返回写锁。而读写锁的具体实现则放在ReadWriteLock接口的实现类中。下面我们来分析下读写锁的具体实现。
ReentrantReadWriteLock
读写锁的定义
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
......
/**
* Creates a new {@code ReentrantReadWriteLock} with
* the given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
}
ReentrantReadWriteLock是ReadWriteLock接口的具体实现类,并实现了ReadWriteLock接口的readLock()方法和writeLock()方法。其内部通过内部类的形式定义了读锁和写锁。
public static class WriteLock implements Lock, java.io.Serializable {
...
//lock()
//tryLock()
//unlock()
//newCondition()
...
}
public static class ReadLock implements Lock,java.io.Serializable {
...
//lock()
//tryLock()
//unlock()
//newCondition() {throw new UnsupportedOperationException()}
...
}
两种锁都实现了Lock接口,核心的API方法大致相同,都是基本的上锁、解锁相关方法,在此不详细展开分析了。不同的一点是WriteLock 支持获取 Condition 条件变量,ReadLock 不支持获取 Condition,尝试获取会抛UnsupportedOperationException。
读写锁的特性
共享与互斥
ReentrantReadWriteLock具有"读读共享,读写互斥,写写互斥"的特性,也就是获取读锁,若写锁被其他线程持有则阻塞;获取写锁,若读锁或写锁被其他线程持有则阻塞。
重入性
读锁和写锁均支持重入。写锁可重入获取读锁(锁降级),但读锁不能升级为写锁。
公平性
支持公平/非公平模式(默认非公平)。非公平模式下写锁优先获取,避免写线程饥饿。
锁降级
允许从写锁降级为读锁(先获取写锁→再获取读锁→释放写锁)。
ReentrantReadWriteLock内部还定义一些监控读写锁状态的相关API,如获取当前读锁持有数getReadLockCount(),检查写锁是否被持有isWriteLocked()等方法。
读操作为什么要加锁
在这里你可能会有一个问题:都是读数据为什么还要加读锁,加了锁不是降低了并发读的能力吗?其实不然,读缓存仍需加锁的主要原因如下:
防止脏读
缓存场景中虽然是读多写少,但仍会有写的时候,当写操作正在更新缓存时,读操作如果不加锁可能读取到未完全更新的中间状态数据,导致脏读。读操作加锁可在读期间阻塞写请求,保证数据一致性。
避免缓存击穿
高并发场景下,若读取过程遇到缓存失效的情况,多个线程可能同时穿透缓存直接访问数据库。通过加锁控制,仅允许一个线程触发数据加载,其余线程等待并复用加载结果,减少数据库压力。
锁降级需求
在写锁持有期间,若需保持数据可见性,可先获取读锁再释放写锁(锁降级)。此时读锁的作用是防止其他写操作干扰,确保降级过程中的数据安全。
热点数据竞争
对高频访问的热点数据,即使无写操作,不加锁可能导致大量线程争抢资源,引发性能抖动。适当的读锁可平衡并发与效率。
经典用法
竟然读写锁在读多写少的场景下显著提升并发性能,那么缓存场景再适合不过了,也是读写锁的经典应用场景。下面是读写锁在缓存中的使用以及锁降级的示例代码:
package com.demo;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class CacheWithReadWriteLock {
private final Map<String, Object> cache = new HashMap<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public Object getData(String key) {
// 1. 先尝试获取读锁
rwLock.readLock().lock();
try {
Object data = cache.get(key);
if (data != null) {
// 缓存命中直接返回
return data;
}
} finally {
//释放读锁
rwLock.readLock().unlock();
}
// 2. 缓存未命中,获取写锁
rwLock.writeLock().lock();
try {
// 双重检查
Object data = cache.get(key);
if (data == null) {
// 从数据库加载
data = loadFromDB(key);
cache.put(key, data);
}
// 3. 锁降级:获取读锁后释放写锁
rwLock.readLock().lock();
return data;
} finally {
// 释放写锁,保持读锁
rwLock.writeLock().unlock();
}
}
private Object loadFromDB(String key) {
/***
* 从数据库查询数据
* ....
*/
return "Data_" + key;
}
}
实现要点说明:
- 读锁优先检查:先通过读锁快速检查缓存是否存在,避免一上来就竞争写锁。
- 写锁独占加载:当缓存失效时,通过写锁保证只有一个线程执行数据库查询。
- 锁降级机制:在写锁持有期间获取读锁,确保数据可见性后再释放写锁。
- 双重检查:避免多个写线程同时通过第一层检查后重复加载数据。
该方案相比互斥锁(如synchronized)的优势在于:
- 读操作完全并行(无锁竞争)
- 写操作仍保持线程安全
- 避免缓存失效时的数据库雪崩效应
总结
本文由ReentrantLock独占的特性可能带来的性能问题,引出读写锁的概念,接着对读写锁的使用场景进行了分析,官方对于使用读写锁是否能带来并发性上的提升是不确定的,受很多因素的制约,倡导开发者根据实际情况合理评估使用。最后对读写锁的核心的API、关键特性、经典使用场景进行讲解,借此对读写锁有了一个比较全面的了解。
1456

被折叠的 条评论
为什么被折叠?



