ReentrantReadWriteLock
从这个类名应该就能猜到这是一个读写锁,当我们在读多写少的场景下可以使用这个类来控制并发。
读和写是互斥的,当个写的线程获取到锁时,其他写和读的线程都无法获取锁,只能等该写线程释放锁之后才可以。
基本结构
ReentrantReadWriteLock实现了ReadWriteLock接口,这个接口只有两个方法:读锁和写锁,返回的都是Lock,说明读锁和写锁都实现了Lock接口。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReentrantReadWriteLock内部同样维护了一个Sync内部类,该类继承了AQS,实现了AQS中的方法控制并发。ReentrantReadWriteLock内部还维护了ReadLock和WriteLock两个内部类,这两个内部类实现了Lock接口,并且这两个内部类中聚合了Sync类。
ReentrantReadWriteLock类也分为公平和非公平:NonfairSync和FairSync
在介绍ReentrantLock时说这是一个互斥锁,通过AQS的state值和Thread来判断锁是否被占有,在读写锁中一个state变量如何表示成读锁或者写锁呢?JDK的一贯做法是将state值按位拆:state是一个int类型变量,高16位表示读锁,低16位表示写锁
写锁
先介绍写锁,因为写锁是一个互斥锁,比较简单,当一个线程获取到写锁时,其他线程只能阻塞。
lock
写锁的获取调用的是WriteLock的lock方法,和ReentrantLock基本一致,首先会调用AQS的acquire方法,然后调用ReentrantReadWriteLock$Sync#tryAcquire方法
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 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;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
- 获取当前线程current
- 获取state值c,根据state值获取独占锁的个数w,即写锁的个数
- 如果state不为0,说明有其他线程占有着锁,需要判断这个占有的锁是读锁还是写锁
- 如果w等于0或者当前线程和独占锁的所持有的线程不相等说明占有的锁是读锁或者是其他写锁,则返回false,当当前线程放入同步队列中
- 走到这一步说明是重入锁,将w和传入的acquire值相加和MAX_COUNT相比,如果大于了MAX_COUNT则抛出错误
- 到这一步是说明该线程可以获取锁了,设置state最新值并返回true
- 如果state值为0说明可以获取锁
- 调用writerShouldBlock方法判断写锁是否需要阻塞,如果为true,则返回结果false,当前线程加入到同步队列中
- 如果writerShouldBlock方法返回false,说明该写线程是可以获取锁的,接着CAS方式设置state值,如果设置失败说明存在竞争,返回结果false,将当前线程加入到同步队列中
- 走到这一步说明已经获取到了锁,那么将当前线程current放入独占锁的持有
writerShouldBlock
该方法是判断写锁是否需要阻塞,这个是需要分别对待:公平锁和非公平锁
非公平锁:非公平锁的话,该方法直接返回false,因为非公平锁是竞争关系,是否能获取到锁由下一步的compareAndSetState方法决定
公平锁:
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
公平锁调用的就是AQS中的hasQueuedPredecessors方法,判断队列中是否含有前驱节点,这个方法在ReentrantLock中介绍过。
unlock
公平锁的解锁底层调用的是Sync的tryRelease方法
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
- 判断当前获取的线程是不是本线程,如果不是抛出异常
- 获取state值并减去传入的releases得到nextc
- 判断nextc是否为0
- 如果为0说明当前线程获取的写锁已经全部释放,将独占锁的占有置为null
- 设置state值
关于写锁的其他几个方法比如lockInterruptibly、tryLock、tryLock(long timeout, TimeUnit unit)、newCondition等方法大体逻辑都是一样的,其中newCondition和ReentrantLock中的newCondition一样
读锁
读锁是一个共享锁,当一个线程获取共享锁的时候,只要state值不为0,那么本线程和其他读线程可以接着获取锁
lock
读锁的获取调用的是AQS中的共享锁获取的逻辑acquireShared,在AQS中的acquireShared方法需要调用子类的tryAcquireShared
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
- 获取当前线程current和state值c
- 判断是否存在写锁,如果存在写锁并且当前线程和独占锁的持有锁不相等,那么返回-1,将当前线程放入同步队列中
- 走到这一步说明不存在写锁,那么就先获取共享锁的数量r
- 如果readerShouldBlock方法返回false,说明读锁不需要阻塞并且r小于MAX_COUNT并且CAS设置state成功
- 如果r等于0,那么将current赋值给firstReader(firstReader表示第一个读锁),firstReaderHoldCount设置为1(firstReaderHoldCount表示第一个读锁获取的数量)
- 如果firstReader和current相等,说明是个重入锁,那么firstReaderHoldCount++
- 以上两个条件不成立,那么就代表这个线程并不是第一个读线程
- 将cacheHoldCounter赋值给rh(cachedHoldCounter是一个HoldCounter类型的变量,用于记录最后一个获取读锁的线程以及它获取的读锁的次数)
- 如果rh为null,或者rh不为null但是当前线程不是此前最后一个获取读锁的线程,那么为当前线程设置一个新的HoldCounter对象,或者获取当前线程HoldCounter对象,存储当前线程以及获取的读锁次数
- 如果rh不为null并且当前线程是此前最后一个获取读锁的线程,更新当前线程在ThreadLocal中的属性值
- 最终当前线程获取的读锁数量都自增1
- 如果上面的if条件不成立,那么调用fullTryAcquireShared方法。
readerShouldBlock
判断读锁是否需要阻塞,也是分为公平锁和非公平锁,对于公平锁的逻辑和写锁是一致的,非公平锁如下:
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
- 如果head不为null,并且head的后继s不为null,并且s是独占模式(写锁),并且s的线程不为null(没被取消),那么返回true
fullTryAcquireShared
fullTryAcquireShared和上面的逻辑大体 相同,只不过是一个死循环,并且会多了一个锁降级的处理
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
...
}
...
}
}
这个方法代码没有贴全,大体逻辑和上面的一致,需要注意这里锁降级的判断:
- 在死循环中如果写锁的数量不为0,并且当前线程和独占锁的持有线程不相等,那么返回-1,如果相等说明获取写锁的线程和当前读锁的线程是同一个线程,逻辑就继续往下走,这里是锁降级的关键
unlock
读锁的释放锁调用的是AQS的releaseShared方法,在AQS中会调用子类的实现tryReleaseShared方法,最终会调用Sync的tryReleaseShared方法
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
上面两个if else逻辑:尝试释放自己保存在本地的锁记录,因为在获取锁的时候,锁记录就加在上本地锁记录中
接着就进入死循环使用CAS的方式设置state值,直至设置成功
HoldCounter含义
说下在读锁的获取与释放中为什么会多出几个变量。独占锁模式中的重入锁使用的是state来维护的,当同一个线程多次获取锁的时候state值就往上加,并且使用exclusiveOwnerThread变量存放获取到锁的线程,在共享锁的模式下也考虑到了重入锁的情况,而这种重入锁并不是像独占锁那样使用state值表示,因为不知道会有多少个线程,所以这时候就使用了ThreadLocal这个变量,这也是为什么会多出这个对象的原因

浙公网安备 33010602011771号