读写锁的特性
相比ReentrantLock,ReentrantReadWriteLock是一种粒度更细,读写分离的锁,更适合高并发但是读多写少的场景。
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock read = readWriteLock.readLock();
private final Lock write = readWriteLock.writeLock();
使用构造函数构造读写锁,并基于读写锁分别构造读锁和写锁,这样读写锁的AQS队列就是共用的
read.lock();
read.unlock();
write.lock();
write.unlock();
可以分别对读锁和写锁加锁,先说结论:
读锁:可重入的共享锁,自己持有读锁,只能重入读锁,其他线程持有写锁的情况下无法加读锁
写锁:可重入的排他锁,自己持有写锁,可以重入读锁和写锁,在其他线程持有读锁或写锁的情况下无法加写锁
基于其特性,进行源码分析:
源码分析
先看读写锁的成员变量
// rw锁中维护了一个读锁一个写锁
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
// 被两个锁共用的AQS队列
final Sync sync;
内部类Sync
内部类Sync继承自AQS,同时扩展了一套状态记录逻辑,这套逻辑类似线程池的ctl逻辑,思路是一致的
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
也是指定32位二进制进行分割,分别表示读锁个数(共享锁个数)和写锁状态(排他锁),分割位是16,共享锁为1左移16位,即左边高16位(16个0)表示共享锁,其最大值是1 << 16 - 1,即16个0+16个1(这里是实际最大值,不考虑二者拼接);
SHARED_UNIT是一个辅助算子,它是1左移16位,即1个1和16个0,左边补0即15个0+1+16个0
MAX_COUNT代表排他锁/共享锁的重入最大次数,即SHARED_UNIT减1,变成16个1.
EXCLUSIVE_MASK(排他锁掩码),顾名思义,负责把共享锁给掩掉,只剩下低16位的共享锁,因此是16个0+16个1(0x0000FFFF),虽然看上去与MAX_COUNT值一样,但其实也是一个辅助算子,没有实际意义
private transient ThreadLocalHoldCounter readHolds;
static final class HoldCounter {
int count; // initially 0
// Use id, not reference, to avoid garbage retention
final long tid = LockSupport.getThreadId(Thread.currentThread());
}
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
内部类形式,HoldCounter记录当前持锁数量和线程id,然后存到ThreadLocal中,表示当前线程的持锁状态
sync排他锁共享锁数量获取
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
获取共享锁数量,即高16位,拿当前状态做>>>操作,无状态右移16位,即左边补16个0,这样就把低位的排他锁顶掉了,高位换成16个0,直接算出来就是低位值,即原来处于高位的共享锁值
获取排他锁数量,用当前状态与EXCLUSIVE_MASK做与运算,做出的结果,原先高位一定是0,因为EXCLUSIVE_MASK高位是16个0,而低位和原先状态c一致,因为EXCLUESIVE_MASK低位是16一个1。这样就排除了高位共享锁的影响。
ReadLock - 读锁的获取与释放
acquireShared
读锁的lock方法调用到acquireShared方法,可以看出来这是一个共享的可重入的方法
if (tryAcquireShared(arg) < 0)
acquire(null, arg, true, false, false, 0L);
它与ReentrantLock一样,先尝试获取,失败后才调用acquire做阻塞,而这里try方法则是在sync里面实现的
tryAcquireShared
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
这里印证了判断,读锁是共享锁,但是不能有排他锁,如果排他锁数量不为0,且持锁线程不是自己,这里尝试获取失败
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT))
第二个if判断,获取读锁数量,不考虑Block公平锁与非公平锁的区分(默认非公平的),如果读锁没到上限,且读锁还能加1成功,即c的高16位加了一个尾1,表示这里获取读锁成功了
获取成功需要修改下当前的状态
if (r == 0)
状态1:获取前读锁是0个,则配置第一个读锁为自己
else if (firstReader == current)
状态2:读锁非0,且第一个读锁是自己线程,这里就给持锁数量+1
状态3:读锁非0,第一个读锁也不是自己
HoldCounter rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
这里做了多次存储,首先取锁持有的内存中的HoldCounter对象,如果不存在,或不是自己,在ThreadLocal里面找自己线程的HoldCounter,取到了,存入锁的变量cached里面,表示当前我占锁了;否则,表示当前自己已经占锁了,那就做一次重入
fullyTryAcquireShared
进一步判断流程
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
这个点是判断别人持有写锁了,直接返回-1,记得在acquireShared方法中返回<0,就走进acquire里面取阻塞了,即当前有写锁,需要取队列里等,否则后面还是判断当前锁状态,修改数量的流程。
tryReleaseShared - 释放读锁
逻辑和ReentrantLock重入锁逻辑是差不多的,通过自旋尝试预计算state重置后的结果,然后通过CAS操作比较和计算,只不过区别是计算基数
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
这里是用1 << 16做运算,普通重入锁是用1
WriteLock - 写锁的获取与释放
lock
public void lock() {
sync.acquire(1);
}
写锁的lock方法直接调的就是sync的acquire方法,是排他的实现
tryAcquire
int c = getState();
int w = exclusiveCount(c);
……
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
……
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
写锁的获取是先通过exclusiveCount()方法从state中拿到低16位,条件判断:
第一个大if,c != 0代表有锁
第一个小if,w == 0即写锁不存在,current != getExclusiveOwnerThread()意味着读锁不是自己,这时就不能加锁
第二个小if已经到了有写锁,且是自己的情况,就考虑重入了
第二个大if,排除了c!=0,说明现在没锁,直接尝试加锁就可以了
tryRelease
写锁因为在低16位,因此直接拿1操作即可,逻辑就和ReetrantLock一致了
读写锁降级
首先降级是什么?
降级指的是拿写锁,拿读锁,释放写锁的过程。拿到写锁,释放写锁,再加读锁不是降级。
为什么要降级?
针对一些并发场景中,需要单线程里面连续修改和读取判断的时候,如果先拿写锁,再释放写锁,再加读锁,很可能被其他线程抢占写锁并修改,自己就可能判断写不成功。
public void processData() {
readLock.lock();
if (!update) {
// 必须先释放读锁,因为读锁排斥本线程写锁重入
readLock.unlock();
// 锁降级修改流程,首先获取写锁
writeLock.lock();
try {
if (!update) {
// 这里指代更新数据
update = true;
}
// 写完成,后面还想使用数据,则加读锁进行降级
readLock.lock();
} finally {
// 写完成,释放写锁
writeLock.unlock();
}
// 锁降级完成,写锁降级为读锁
}
try {
// 使用数据的流程(略)
} finally {
// 读完成,释放读锁
readLock.unlock();
}
}
评论区