现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁(读多写少)。在没有写操作的时候,多个线程同时读一个资源没有任何问题(不存在线程安全问题),并且也应该允许多个线程同时读取共享资源(读读共享);但是,如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写操作了(读写,写写互斥),否则可能会出现线程安全问题。
思考:上述不管哪种情况,控制访问资源如果都通过ReentrantLock独占锁来解决,性能上会大打折扣,因为读读操作是不需要加锁的。针对这种场景,有没有比ReentrantLock更好的方案?那就是读写锁。
总结:读读不存在线程安全问题。写读、写写操作存在线程安全问题。
如何设计一把读写锁?(读读共享,写读、写写互斥)
利用AQS
互斥:tryAcquire、tryRelease 写锁 state=0 1
共享:tryAcquireShared、tryReleaseShared 读锁 state!=0
怎么保证读写互斥?
通过状态来判断。但是,这里有两个状态:读状态 、写状态
ReentrantReadWriteLock是基于AQS实现的,而AQS中只有一个state变量。如何通过一个变量控制两种状态?高地位
AQS中的state是int类型,在java中占四个字节
00000000 00000000 00000000 00000000 高16位 低16位。高16位控制读,低16位控制写
读写互斥,加写锁时怎么判断是否有读锁?
通过Java位运算实现
如果要实现可重入,怎么做?
对于写锁,因为是独占的,只会有一个线程占用写锁,所以,只允许占用写锁的线程重入,通过状态+1即可实现
对于读锁,允许多个线程重入,而通过state的高16位记录每个线程的重入次数是不现实的,高16位只能记录获取读锁的线程个数。那怎么办呢?可以通过ThreadLocal(线程私有的)来解决。
1. 读写锁介绍
2. ReentrantReadWriteLock
线程进入读锁的前提条件:
- 没有其他线程的写锁;
- 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个;
线程进入写锁的前提条件:
- 没有其他线程的读锁;
- 没有其他线程的写锁;
而读写锁有以下三个重要的特性:
- 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;
- 可重入:读锁和写锁都支持线程重入。以读写线程为例:读线程获取读锁后,能够再次获取读锁。写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁;
- 锁降级:遵循获取写锁、再获取读锁最后释放写锁的次序,写锁能够降级成为读锁;
2.1. ReentrantReadWriteLock的使用
2.1.1. 读写锁接口ReadWriteLock
接口中有一对方法,分别是获得读锁和写锁 Lock 对象。
2.1.2. ReentrantReadWriteLock类结构
ReentrantReadWriteLock是可重入的读写锁实现类。在它内部,维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 Writer 线程,读锁可以由多个 Reader 线程同时持有。也就是说,写锁是独占的,读锁是共享的。
2.1.3. 如何使用读写锁
1. 使用方式
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock r = readWriteLock.readLock();
private final Lock w = readWriteLock.writeLock();
// 读操作上读锁
public Data get(String key) {
r.lock();
try {
// TODO 业务逻辑
}finally {
r.unl