文章目录
概述
ReentrantReadWriteLock
(读写锁)是对于ReentranLock
(可重入锁)的一种改进,在可重入锁的基础上,进行了读写分离。适用于读多写少
的场景,对于读取
和写入
操作分别加锁。其中读取与读取操作同步
,读取和写入,写入和写入操作互斥
。并且支持写锁降级的机制。
ReentrantReadWriteLock
的体系结构:
- 实现了ReadWriteLock接口,定义了读锁和写锁的模版方法,在ReentrantReadWriteLock分别进行实现(ReentrantReadWriteLock内部实现了两把锁)。
- sync属性,实现了AQS的规范。
- ReadLock和WriteLock都实现了Lock接口,Lock接口作为可重入锁的模版,定义了共有的行为。
实现了读写锁的规范接口
sync属性,是一个静态内部类,继承了AQS,AQS是一种规范,抽象的队列式同步器
MESA管程模型,入口等待队列用于互斥,条件变量等待队列用于同步
ReentrantReadWriteLock中的写锁,实现了Lock接口
ReentrantReadWriteLock中的读锁,同样也实现了Lock接口
Lock接口,规范了锁的实现
ReentrantReadWriteLock 同样支持公平锁和非公平锁的实现
一、状态位设计
ReentrantReadWriteLock
的内部类Sync
,重写了父类AQS的acquireShared方法,定义了读锁的共享、可重入特性,但是AQS的state状态位,只能表示同步状态,不能同时维护读锁、写锁的状态。在读写锁的实现中,对于state状态位,实现了按位切割
的算法:
- 低 16 位(低 2 字节):存储 写锁的
重入次数
。 - 高 16 位(高 2 字节):存储 读锁的
获取次数
。
为什么写锁存储的是重入次数
?写锁通常是互斥的,但有时一个线程可能会多次请求写锁(即重入)所以需要统计的是,某个线程重入了几次写锁
。读锁存储的是获取次数
?读锁是同步的,一般不会某个线程多次请求读锁,所以需要统计的是当前有多少个线程持有读锁
Sync类中,定义了状态位的相关属性,以及获取读,写计数的方法:
java
// 读锁偏移 16 位,读锁存储在 state 的高 16 位。
static final int SHARED_SHIFT = 16;
// 读锁的单位值(1 左移 16 位,即 65536)
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//最大值 (1 左移 16 位 - 1,即 65535)
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//用于提取 低 16 位。
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count */
//从 state 变量中提取高 16 位,即 读锁的获取次数。
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
// 按位与 & 操作 提取 低 16 位,即 写锁的重入次数。
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
用一个案例进行说明:
- 假设初始 state = 0(没有读锁和写锁)
state = 0x00000000 (0000 0000 0000 0000 0000 0000 0000 0000)
- 写锁(低 16 位):exclusiveCount(state) = state & 0000 0000 0000 0000 1111 1111 1111 1111 = 0
- 读锁(高 16 位):sharedCount(state) = state >>> 16 = 0
- 假设写锁重入 3 次,此时 state = 3
state = 0x00000003 (0000 0000 0000 0000 0000 0000 0000 0011)
- 写锁(低 16 位):
exclusiveCount(state) = state & 0000 0000 0000 0000 1111 1111 1111 1111
= 00000000 00000000 00000000 00000011 & 0000 0000 0000 0000 1111 1111 1111 1111
= 00000000 00000000 00000000 00000011 (3)
- 读锁(高 16 位):
sharedCount(state) = state >>> 16
= 00000000 00000000 00000000 00000011 >>> 16
= 00000000 00000000 00000000 00000000 (0)
二、读锁
读锁的特点是共享
,也就是读锁和读锁之间不互斥,并且可重入
。那么在源码的层面,是如何定义共享、可重入的?
采用sync的acquireShared
案例代码:
java
public class Demo1 {
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
public static void main(String[] args) {
readLock.lock();
System.out.println("第一次获取到读锁");
try {
readLock.lock();
System.out.println("第二次获取到读锁");
try {
System.out.println("重复获取读锁");
}finally {
readLock.unlock();
System.out.println("第二次获取到读锁被释放");
}
}finally {
readLock.unlock();
System.out.println("第一次获取到读锁被释放");
}
}
}
![](https://i-blog.csdnimg.cn/direct/d8f8c0c15d5849be938c9837e598e8a2.png)
读锁的加锁操作:
java
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
//获取当前线程
Thread current = Thread.currentThread();
//获取当前线程的状态标记
int c = getState();
//已经有了写锁,并且写锁的拥有者不是当前线程
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
//当前有写锁且不是当前线程持有的写锁,返回 -1,表示获取读锁失败。
return -1;
//得到读锁的获取次数
int r = sharedCount(c);
//检查当前线程是否需要等待
//确保当前读锁的获取次数 r 小于最大限制 MAX_COUNT
//使用 CAS 操作来更新 state。
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//读锁的获取次数为0
if (r == 0) {
//标记当前线程为第一个获取到的
firstReader = current;
//记录第一个获取到读锁的线程的获取次数
firstReaderHoldCount = 1;
//读锁的获取次数不为零,并且当前线程和第一个获取到读锁的线程相同
} else if (firstReader == current) {
//第一个获取到读锁的线程的获取次数增加
firstReaderHoldCount++;
//读锁的获取次数不为零,并且当前线程不是第一个获取到读锁的线程
} else {
//从缓存中获取持有计数器
HoldCounter rh = cachedHoldCounter;
//如果缓存为空或者当前线程的 tid 不匹配
if (rh == null || rh.tid != getThreadId(current))
//则从 readHolds 中获取新的计数器。
cachedHoldCounter = rh = readHolds.get();
//如果 rh.count == 0,说明当前线程刚开始持有读锁。
else if (rh.count == 0)
//更新计数器。
readHolds.set(rh);
//增加当前线程的读锁计数。
rh.count++;
}
return 1;
}
//如果前面的 CAS 操作失败,进行 排队等待,直到条件满足为止。
return fullTryAcquireShared(current);
}
这一段尝试获取读锁的代码,精髓在于使用 CAS 操作来更新 state,并且记录读锁的获取次数,是将第一个线程和后续线程分开计数的。
三、锁降级机制
如果一个线程,先获取到了写锁,然后再去获取读锁,最后释放写锁,写锁能够降级成为读锁,即:一个线程在持有写锁的情况下,将写锁转换为读锁,即允许线程在修改资源之后,在不释放锁的情况下继续读取资源
。(上述的操作,只针对当前线程)
java
public class Demo2 {
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
public static void main(String[] args) {
//先获取到了写锁
writeLock.lock();
System.out.println(Thread.currentThread().getName() + "获取到写锁");
try {
//再去获取读锁
readLock.lock();
System.out.println(Thread.currentThread().getName() + "获取到写锁后,又获取到了读锁");
}finally {
//最后释放写锁
writeLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了写锁");
}
try{
System.out.println("业务代码执行");
}finally{
readLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了写锁后,释放了读锁");
}
}
}
锁降级机制,特别适用于:
- 数据修改后再查询:当某个线程对共享资源进行了修改(如数据库更新),并且希望读取最新的数据时,直接降级为读锁可以避免不必要的锁切换。
- 避免频繁锁获取:在某些高并发场景中,多个线程需要频繁读取共享数据,但在一开始线程会进行写操作,降级为读锁可以避免重新获取写锁并减少锁的开销。
为什么要在获取写锁和释放写锁之间,去获取读锁呢?主要是为了保证数据的一致性。避免先释放写锁-再获取读锁
的过程中,其他线程抢先一步获取到了写锁修改了数据。获取读锁-释放写锁
,那么其他线程想要获取写锁修改数据,因为读-写锁之间的互斥,所以其他线程将被阻塞。
读锁尝试获取锁的代码中,下面的片段就是锁降级机制的体现
,即已经有了写锁,并且当前线程是该写锁的持有者,那么可以继续获取读锁,不会返回-1失败:
java
//已经有了写锁,并且写锁的拥有者不是当前线程
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
//当前有写锁且不是当前线程持有的写锁,返回 -1,表示获取读锁失败。
return -1;
四、写锁
在源码的层面,通过AQS的acquire方法,保证不同线程间
写锁-写锁,读锁-写锁之间的互斥性。
案例代码:
java
public class Demo3 {
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
public static void main(String[] args) {
writeLock.lock();
System.out.println(Thread.currentThread().getName() + "获取到了写锁");
try {
writeLock.lock();
System.out.println(Thread.currentThread().getName() + "再次获取到了写锁");
try {
System.out.println("....");
}finally {
writeLock.unlock();
System.out.println(Thread.currentThread().getName() + "再次释放了写锁");
}
}finally {
writeLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了写锁");
}
}
}
同一线程间写锁间(写锁和读锁间)不互斥
java
public class Demo3 {
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
public static void main(String[] args) throws InterruptedException {
writeLock.lock();
System.out.println(Thread.currentThread().getName() + "获取到了写锁");
Thread.sleep(3000);
try {
new Thread(() -> {
writeLock.lock();
System.out.println(Thread.currentThread().getName() + "再次获取到了写锁");
try {
System.out.println("....");
}finally {
writeLock.unlock();
System.out.println(Thread.currentThread().getName() + "再次释放了写锁");
}
}).start();
}finally {
writeLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了写锁");
}
}
}
不同线程间的写锁间互斥(写锁和读锁间也互斥)
java
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
//获取当前线程
Thread current = Thread.currentThread();
//获取状态
int c = getState();
//提取当前状态 c 中的写锁计数
int w = exclusiveCount(c);
//状态不为0 意味着锁当前处于非空状态
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
//写锁重入次数为0 或者 当前线程不是写锁的持有者
//说明当前线程没有持有写锁,或者存在读锁或其他线程持有写锁(实现不同线程间的读-写 写-写互斥)
if (w == 0 || current != getExclusiveOwnerThread())
//返回加锁失败
return false;
//重入次数超过限制
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
//状态 + 重入次数
setState(c + acquires);
//加锁成功
return true;
}
//状态为0
//应该被阻塞 或 CAS 状态 + 重入次数 累加失败
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
//返回加锁失败
return false;
//设置当前线程为锁的持有者
setExclusiveOwnerThread(current);
return true;
}
总结
线程可以获取到读锁的条件:
- 没有其他线程获取到写锁。
- 有写锁,但是是当前线程持有(锁降级机制)
线程可以获取到写锁的条件:
- 没有其他线程获取到读锁。
- 没有其他线程获取到写锁。
对于同一个线程而言:读线程获取读锁后,能够再次获取读锁(不能再获取到写锁,即不支持锁升级)。写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。