显式锁ReentrantReadWriteLock
- ReentrantReadWriteLock是读写锁,同一时间允许多个线程获取读锁,但是写锁同一时间只能允许一个线程获取。并且会阻塞其余的读写线程。简单表示就是读读共享,读写互斥,写写互斥,适合读多写少的应用场景。
一、ReadWriteLock接口
- ReadWriteLock是读写锁的接口,接口定义了读锁和写锁的获取方法,主要实现类是ReentrantReadWriteLock
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*/
Lock writeLock();
}
- 读写锁的主要特性
公平性:支持公平性和非公平性。
重入性:支持重入。读写锁最多支持65535个递归写入锁和65535个递归读取锁。
锁降级:遵循获取写锁,再获取读锁,最后释放写锁的次序,如此写锁能够降级成为读锁。
二、ReentrantReadWriteLock源码结构
- ReentrantReadWriteLock是ReadWriteLock接口的主要实现类,是读写锁。
2.1 初识
- 我们先看源码中的部分注释,将其大概翻译如下:
1.非公平模式是默认的模式,该模式下获取读锁和写锁的顺序是不确定的,因此在持续争用锁的时候可能会导致某一个读或者写线程一直获取不到锁,但是通常拥有更高的吞吐量
2.公平模式下,使用的是按照到达的顺序的策略(即先到先得)。当一个锁被释放时,一个写线程或者一组读线程都可能获取到锁,这取决于谁等待的更久。
3.当线程去获取公平的读锁的时候,如果写锁被持有或者又有写锁在等待,那么线程会阻塞等待直到写锁被释放,或者等待写锁的线程获取然后又释放了锁。如果获取写锁的线程放弃等待,获取读锁的线程成为了等待最久的线程,那么获取读锁的线程就会得到读锁。
4.当线程去尝试获取公平的写锁的时候,会阻塞直到读锁和写锁都释放(读写互斥,写写互斥,加锁此时没有前面没有其他线程等待)。
5.需要注意的是ReadLock#tryLock()和WriteLock#tryLock()不遵循公平的设置,会尝试快速获取锁,而不会排队在已经等待的线程后面。
- ReentrantReadWriteLock是读写锁的实现,我们先了解它,然后分析其源码,要点如下:
1.ReentrantReadWriteLock实现了ReadWriteLock接口,具备读写锁的功能,他的内部有ReadLock和WriteLock这两个内部类来实现读写锁,这两个类都是Lock接口的实现类,在ReentrantReadWriteLock构造方法调用的时候,就会初始化这2把读锁和写锁。
2.ReentrantReadWriteLock内部锁机制是基于AQS实现的,内部的Sync类继承AQS,加锁解锁操作都是基于AQS的state来实现的,读锁和写锁都持有一个Sync实例,但是是Sync的不同子类,对应着公平和非公平2种模式的实现类。ReentrantReadWriteLock内部的Sync有2个子类一个是NonfairSync另一个是FairSync,分别代表非公平模式和公平模式的Sync,在初始化的时候通过布尔参数指定是哪一种模式,默认非公平模式,非公平模式下,线程间竞争锁的时候,不能保证先等待锁的线程先获取到锁,公平模式可以保证。实现原理上,对于公平锁在获取锁的时候总是会先判断自己是否有前驱节点,有的话自己就不会竞争锁,让前驱先获取到锁。
4.ReentrantReadWriteLock中读锁和写锁的最大区别的一点在于读读可以共享,读写互斥,写写互斥。读读共享的实现机制在于读锁获取锁的时候,是调用的AQS的共享模式获取锁,因而可多个读线程获取锁,二写锁是独占模式。
5.读锁和写锁内部都包同一个Sync实例,默认是非公平模式NonfairSync的实例,获取和释放锁本质都是通过内部的这个NonfairSync实例操作state变量。
2.2 Sync
- ReentrantReadWriteLock内部的Sync源码比较多,超过400行,虽然都是继承自AQS,但是它比之前的CountDownLatch或者Semaphore内部的Sync类复杂很多。这里只能对其核心代码和思想进行分析。在ReentrantLock里面用AQS的state表示锁的重入次数,在ReentrantReadWriteLock中因为更加复杂,有读锁和写锁的区分,因此使用“按位切割使用”的方式来维护锁的状态,将变量切分为高16位表示读,低16位表示写。通过位运算来计算:
//获取读状态:S >>> 16 (无符号补 0 右移 16 位)
//获取写状态:S & 0x0000FFFF(将高 16 位全部抹去)
//切分位数
static final int SHARED_SHIFT = 16;
//
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//表示0XFFFF,与运算时保留低16位,用来获取写状态
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count */
//获取读状态,右移16位获取到高位,高16位表示读,写锁的独占的,但是读锁涉及到多个线程之间的共享,并且支持重入,所以内部使用HoldCounter来为每个线程持有的读锁单独计数
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; }
PS:在Sync内部通过state变量来表示持有锁的数量,从而实现了锁的可重入和读写锁的特性,结合前面的高低位分离,
有巧妙的将读写锁的细节分开,互不干扰。
2.2.1 构造方法
- Sync内部通过ThreadLocal机制来保存每个线程的读锁持有个数,会在构造方法中对其进行初始化。
/**
* 每一个线程的读锁持有的计数器,使用ThreadLocal来保存,线程之间隔离
* 并且会缓存在cachedHoldCounter变量里面,
* 这个类比较简单,就是一个线程id和一个int计数变量
*/
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
/**
*保存线程读锁持有数量的ThreadLocal变量,这里没有直接使用ThreadLocal,是继
* 承ThreadLocalHoldCounter,
*/
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
/**
* ThreadLocalHoldCounter类实例,用来保存每个线程锁持有的读锁数量,
* 只会在构造方法和读锁中初始化,当线程持有的读锁减为0就会移除
*/
private transient ThreadLocalHoldCounter readHolds;
/**
* 最后一个获得读锁的线程的HoldCounter的缓存
*/
private transient HoldCounter cachedHoldCounter;
/**
* 第一个获取读锁的线程
* 第一个获取读锁的线程的HoldCount对象
*/
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
Sync() {
//在构造方法中初始化readHolds
readHolds = new ThreadLocalHoldCounter();
setState(getState()); // ensures visibility of readHolds
}
2.2.2 readerShouldBlock和writerShouldBlock
- 这是2个返回布尔类型的方法,表示在获取读锁或者写锁的时候,是否应该阻塞,是由子类的读锁和写锁自行实现的,
这里定义的是抽象方法,显然2种锁,这个逻辑是不一样的,读锁中读读共享,但是写锁中读写是互斥的。
/**
* 尝试获取读锁的时候,是否应该阻塞,由子类实现
*/
abstract boolean readerShouldBlock();
/**
* 尝试获取写锁的时候,是否应该阻塞,由子类实现
*/
abstract boolean writerShouldBlock();
2.2.3 tryAcquireShared读模式获取
- 这是读模式下获取共享状态的方法,在Sync中重写了该方法,在AQS中是没有实现的
//共享模式获取共享状态
protected final int tryAcquireShared(int unused) {
/*
* 共享式获取,对应读模式下获取锁
*1.如果写锁被持有,则获取失败
*2.不满足1,那么线程尝试获取读锁,根据排队模式,确定是否需要阻塞,
* 如果不需要这是,使用CAS尝试获取state变量并在获取成功后更新,但是
* 这个步骤没有检查重入获取读锁,这被推迟到了完整版本,避免检查holdCount,
* 考虑到更多场景是非重入的情况
*3.如果步骤2失败了,不管是因为锁统计饱和,或者是线程不能进行去获取锁(比如被写锁阻塞)
* 或者CAS失败,跳转到full retry loop(这里不好解释,看代码就是进入
* fullTryAcquireShared方法里面自旋)
*/
Thread current = Thread.currentThread();
int c = getState();
//1.写锁被持有,并且不是被自身持有,说明写锁被其他线程持有,返回-1
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
//能够跳过步骤1的逻辑,说明要么写锁被持有但是是被自己持有,要么写锁没有被持有,
//2.获取读锁值,
int r = sharedCount(c);
//3.如果不要被阻塞(公平模式下没有前驱),并且读写未达到上限65535,并且设置state成功,就进入里面的逻辑
//反之就要调用fullTryAcquireShared方法
//修改高16位的状态,所以要加上2^16
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
//3.1 如果读锁为0,说明自己是第一个,设置firstReader和firstReaderHoldCount
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
//3.2 如果自己是第一个读线程,但是r!=0,那就firstReaderHoldCount加1
} else if (firstReader == current) {
firstReaderHoldCount++;
//3.3如果r!=0,并且自己不是第一个线程,那么自己就考虑之前获取锁的最后一个线程是不是自己,
} else {
HoldCounter rh = cachedHoldCounter;
//如果自己不是之前获取锁的最后一个线程,那么将自己的持有锁的数量保存到cachedHoldCounter
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
//到这里,说明之前获取锁的最后一个线程就是自己,并且rh.count还是0
//那就将cachedHoldCounter保存到readHolds,也就是将自己的持有锁的数量保存到cachedHoldCounter
//不明白这里怎么会有为0的情况呢?之前最后获取锁的线程是自己,那么自己持有锁,count至少为1,如何为0?
else if (rh.count == 0)
readHolds.set(rh);
//如果不满足前面2个逻辑,说明之前获取锁的最后一个线程就是自己,并且rh不是null,并且rh.count也不是0,
//那么就直接count自增表示持有的锁加1
rh.count++;
}
return 1;
}
//4.第三点的条件没有满足,进入fullTryAcquireShared方法,在方法里面其实就是不断自旋
//直到
return fullTryAcquireShared(current);
}
2.2.4 tryAcquire写模式获取
- 这是写模式下获取共享状态的方法,在Sync中重写了该方法,在AQS中是没有实现的
//独占模式获取共享状态
protected final boolean tryAcquire(int acquires) {
/**
* 独占式获取,对应写模式下获取锁
*1.如果读锁或者写锁非零,并且锁不是被当前线程持有,那么获取失败(因为写写互斥,读写也是互斥)
*2.如果计数饱和,失败
*3.不满足1,2,并且是可重入锁,排队策略允许的话,线程就会尝试获取锁,获取成功会
* 将自身设置为锁的拥有者线程
*/
Thread current = Thread.currentThread();
int c = getState();
//1.获取写锁状态,低16位表示写
int w = exclusiveCount(c);
//2.写锁状态不为0,
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
//逻辑1:若写锁状态w为0,但c!=0,说明此时高位读模式不为0,读写互斥,这里获取写锁应该失败返回false,上面的源码注释提示的比较清楚
//逻辑2:当前线程不是拥有者线程,此时c!=0,说明有其他线程要么在读要么在写,因此也要返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//3.到这里说明w不是0代表写锁被持有,并且自身是拥有者线程,很明显自己就是写锁的持有者,此时应该是写锁的重入,
//那么就需要判断,重入次数是否到最大值了,超过的话抛异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
//4.没有超过就是锁重入,这里源码有关键的注释提示"Reentrant acquire"
setState(c + acquires);
return true;
}
//6.如果c为0,说明读锁写锁都是0(c是0,那么高低位肯定都是0),这时候根据writerShouldBlock判断当前是否需要阻塞,
// 非公平模式返回false是不阻塞的,公平模式则在有前驱的情况下要阻塞,
//逻辑1:writerShouldBlock为false,说明是非公平模式,那就尝试CAS设置state,设置失败就返回false,设置成功
// 就继续后面的逻辑
//逻辑2:如果writerShouldBlock为true,说明是公平模式下并且有前驱节点,因此需要阻塞排队,返回false
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
//7.如果是前面的逻辑1,那就设置拥有者线程为当前线程,并返回true
setExclusiveOwnerThread(current);
return true;
}
2.2.5 tryReleaseShared读模式释放
- 这是读模式下获取共享状态的方法,在Sync中重写了该方法,在AQS中是没有实现的
//共享模式释放共享状态,对应读模式下释放锁
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//1.如果当前线程是第一个读线程
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
//第一个读线程,并且之前持有的读锁次数是1,现在释放了,那就将firstReader设为null
if (firstReaderHoldCount == 1)
firstReader = null;
else
//第一个读线程,并且之前持有的读锁次数不是1,那就减一
// (不是1肯定大于1,不会小与等于0,因此释放之前肯定获取过)
firstReaderHoldCount--;
} else {
//2.如果当前线程不是第一个获取读锁的线程,那就获取最后一个线
//程的HoldCounter(最后一个线程的HoldCounter缓存在cachedHoldCounter)
HoldCounter rh = cachedHoldCounter;
//2.1 如果为null,或者最后一个线程不是自己
if (rh == null || rh.tid != getThreadId(current))
//那就将自身的持有锁次数更新到rh
rh = readHolds.get();
//逻辑1:如果最后一次那个线程就是自己(也就是自己连续2次获取读锁),那么就先记下下自己持有锁的次数
//逻辑2:如果最后一次的线程不是自己,那么就几下自己持有的读锁的次数
//不管逻辑1还是逻辑2,最终都是记下自己的持有锁次数,只不过如果自己是连续获取锁,处理逻辑不一样
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
//之前在上面记下了自己持有锁的次数,如果次数大于1,现在在这里就释放一次,如果是1或者0,就移除自身持有锁的
// 记录,如果是0或者负数,还会抛出一个异常
--rh.count;
}
//使用CAS更新同步状态
for (;;) {
int c = getState();
//这里将state设置为一个state-65536的数字是模式目的呢?
//因为读锁是再高16位,低16位是写锁,低16为值是65535,首先确认一点,此时的低位全部都是0,因为在释放读锁
//而读锁和写锁是互斥的,不能同时大于0
// 情况1:如果state-65536=0,说明state为65536,高位为1低位全部是0,此时只有一个读锁,因此释放之后就没有锁了,
// 后续的读锁线程和写锁线程可以继续竞争锁,这也是下面nextc等于0的情况
//情况2:如果nextc>0,说明高位大于1,即有多把读锁,因此释放之后,依然还有其他读锁占有,加入c是65538,那么
// nextc是2,到此我们知道nextc就是剩余的读锁数量。
//情况3:nextc<0,这种情况不存在,因为至少有一把读锁,高位至少为1,高位为1时,c就是65536
//到这我们明白了这里其实就是重新设置读锁的数量(需要理解读写锁高低位的设计,否则有点不明白),netxc是释放后
//读锁的数量,如果释放后为0,返回true,反之返回false,返回值的含义就是当前锁是否还被持有。(这对读线程没有影响,
// 但是写线程很关心这个,如果被持有,读线程可以继续进来写线程却不可以,如果没有被持有,读锁线程就竞争)
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;
}
}
2.2.6 tryRelease写模式释放
- 这是写模式下获取共享状态的方法,在Sync中重写了该方法,在AQS中是没有实现的
/**
* 独占式释放,对应写锁的释放锁
*/
protected final boolean tryRelease(int releases) {
//1.如果不是当前线程持有锁,抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//2.因为是独占式释放,因此是线程安全的,直接计算释放后的state变量值
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
//3.如果释放之后是0,说明已经解锁了,那就设置占有线程为null
if (free)
setExclusiveOwnerThread(null);
//4.反之说明还没解锁,修改state即可
setState(nextc);
return free;
}
2.2.7 tryWriteLock尝试获取写锁
- tryWriteLock尝试获取写锁
/**
* 尝试获取写锁,获取成功返回true,反之返回false,不会阻塞,无视公平模式下的排队原则
*/
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c != 0) {
//1.获取写锁数量
int w = exclusiveCount(c);
//2.1如果w=0,但是c不为0,说名读锁被占用,返回false
//2.2如果w不为0,那么此时有可能高位读锁为0,那么此时再判断自己是不是拥有者线程,如果不是就返回false,
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//3.写锁达到上限,抛出异常
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
//4.到这里说明c是0,或者是c为0,但是不是前面那三种逻辑,那么此时就CAS尝试修改state变量,
//修改失败返回false,不阻塞
if (!compareAndSetState(c, c + 1))
return false;
//5.修改成功,设置自身为拥有者线程
setExclusiveOwnerThread(current);
return true;
}
2.2.8 tryReadLock尝试获取读锁
- tryReadLock尝试获取读锁
/**
* 尝试获取读锁。获取成功返回true,反之返回false,不会阻塞,无视公平模式下的排队原则
*/
final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {
int c = getState();
//1.写锁被占有,并且不是自己占用,返回fasle
// (如果是自己占有写锁,再获取读锁,是要成功的,这就是锁降级)
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return false;
//2.获取读锁数量
int r = sharedCount(c);
//3.读锁超限,抛异常
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//4.读锁没有超限,CAS设置state
if (compareAndSetState(c, c + SHARED_UNIT)) {
//4.1读锁状态为0,说明自己是第一个获取读锁的线程,设置相关变量
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
//4.2自己是记录的第一个获取读锁的线程,同样修改firstReaderHoldCount自增1
} else if (firstReader == current) {
firstReaderHoldCount++;
//4.3else逻辑说明读锁状态不为0,已经被其他线程占有,且第一个线程并不是自己
} else {
//下面需要处理最近最后一次获取锁的线程不是当前线程和是当前线程的2种情况
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 true;
}
}
}
2.3 ReadLock
- ReadLock是读锁,读锁在构造方法会将Sync对象穿进去,提供的锁相关的接口就是调用Sync的操作state的方法。
public static class ReadLock implements Lock, java.io.Serializable {
private final java.util.concurrent.locks.ReentrantReadWriteLock.Sync sync;
/**
* 构造方法,传入Sync对象
*/
protected ReadLock(java.util.concurrent.locks.ReentrantReadWriteLock lock) {
sync = lock.sync;
}
/**
* 加锁方法,以共享方式获取同步状态变量state,因此读锁是共享的
*/
public void lock() {
sync.acquireShared(1);
}
/**
*支持中断的获取锁
*/
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
/**
* 这里注意,尝试获取锁,即使是公平锁也会抢占,忽略排队的线程,
* 目的是快速尝试是否能够获取锁,
*/
public boolean tryLock() {
return sync.tryReadLock();
}
/**
* 支持超时的tryLock
*/
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
/**
* 解锁,释放同步状态state
*/
public void unlock() {
sync.releaseShared(1);
}
/**
* 读锁不支持Condition,Condition是用来做线程的等待和唤醒的,对读锁而言,
* 一把读锁多个线程是共享的,不存在相互阻塞唤醒的说法
*/
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
2.4 WriteLock
public static class WriteLock implements Lock, java.io.Serializable {
private final java.util.concurrent.locks.ReentrantReadWriteLock.Sync sync;
/**
* 构造方法
*/
protected WriteLock(java.util.concurrent.locks.ReentrantReadWriteLock lock) {
sync = lock.sync;
}
/**
* 加锁,独占式获取同步状态state
*/
public void lock() {
sync.acquire(1);
}
/**
* 响应字段的加锁方式
*/
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
/**
* 尝试获取锁,
*/
public boolean tryLock( ) {
return sync.tryWriteLock();
}
/**
* 支持超时的尝试获取锁
*/
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
/**
* 解锁
*/
public void unlock() {
sync.release(1);
}
/**
* 返回Condition对象
*/
public Condition newCondition() {
return sync.newCondition();
}
/**
* 判断锁是否被当前线程持有
*/
public boolean isHeldByCurrentThread() {
return sync.isHeldExclusively();
}
/**
* 判断当前线程重入写锁的次数
*/
public int getHoldCount() {
return sync.getWriteHoldCount();
}
}
2.5 其他方法
- ReentrantReadWriteLock还提供了很多其他的方法,这些方法和ReentrantLock非常类似,比如获取等待线程之类的,
几不一一解析了,基本上看方法名称就能大概猜到方法的作用。
/**
* 判断公平模式
*/
public final boolean isFair() {
return sync instanceof java.util.concurrent.locks.ReentrantReadWriteLock.FairSync;
}
/**
* 获取锁的拥有线程
*/
protected Thread getOwner() {
return sync.getOwner();
}
/**
* 返回读锁的持有个数,用于系统状态检测,不是用于线程同步
*/
public int getReadLockCount() {
return sync.getReadLockCount();
}
/**
* 判断写锁是否被其他线程持有
*/
public boolean isWriteLocked() {
return sync.isWriteLocked();
}
/**
* 判断写锁是否被当前线程持有
*/
public boolean isWriteLockedByCurrentThread() {
return sync.isHeldExclusively();
}
/**
* 判断当前线程重入写锁的次数
*/
public int getWriteHoldCount() {
return sync.getWriteHoldCount();
}
/**
* 判断读锁被持有的数量
*/
public int getReadHoldCount() {
return sync.getReadHoldCount();
}
/**
* 获取等待写锁的线程的集合,只是一个估计值
*/
protected Collection<Thread> getQueuedWriterThreads() {
return sync.getExclusiveQueuedThreads();
}
/**
* 获取等待读锁的线程的集合,只是一个估计值
*/
protected Collection<Thread> getQueuedReaderThreads() {
return sync.getSharedQueuedThreads();
}
/**
* 判断是否有线程等待锁(读锁或者写锁)
*/
public final boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
/**
* 判断某线程是否在等待锁(读锁或者写锁)
*/
public final boolean hasQueuedThread(Thread thread) {
return sync.isQueued(thread);
}
/**
* 获取等待锁线程的数量估计值
*/
public final int getQueueLength() {
return sync.getQueueLength();
}
/**
* 判断是否有线程等待在Condition上
*/
public boolean hasWaiters(Condition condition) {
if (condition == null)
throw new NullPointerException();
if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
throw new IllegalArgumentException("not owner");
return sync.hasWaiters((AbstractQueuedSynchronizer.ConditionObject)condition);
}
三、锁降级
3.1 锁降级顺序
- 读写锁是支持锁降级的,也就是我原本获取到写锁了,但是我可以将写锁降级为读锁,降级之后可以允许其他的读线程进来,降级的过程如下:
1.获取写锁 --> 2.获取读锁 --> 3.释放写锁
- 注意第二步和第三步是不可以交换顺序的,假设按照:1.获取写锁 --> 2.释放写锁 --> 3.获取读锁 这样的顺序,那么在第二和第三步之间可能某个写线程进来了修改了数据,
本线程在第三步到读锁看到的数据,和第2步释放写锁之前看到的数据很可能不一致。但是按照1.获取写锁 --> 2.获取读锁 --> 3.释放写锁 这样的顺序,整个过程写线程都是被阻塞的,
释放写锁之后,其他读线程就可以进来,提高读的并发效率。
3.2 锁降级代码
- 我们看看锁降级在ReentrantReadWriteLock中的代体现,既然要遵循1.获取写锁 --> 2.获取读锁 --> 3.释放写锁 这样的获取顺序,那么降级的代码逻辑应该是在获取读锁和释放写锁
里面。如果说线程已经执行了第一步获取到了写锁,因为写锁是独占的,那么其获取读锁是一定会成功的(可重入),否则1->2之间就会死锁了。 - 读写锁重入特性需要在锁的使用代码中进行控制,保证加锁的顺序如3.1所描述的,在源码中体现在一个线程在已经获得了写锁的情况下,是可以继续获取读锁的,这也是保证锁降级的关键。
3.3 fullTryAcquireShared
- fullTryAcquireShared是tryAcquireShared(int unused) 方法的自旋重试的逻辑,里面的逻辑和tryAcquireShared有很多相似之处,也支持在占有写锁的状态下继续获取读锁。
final int fullTryAcquireShared(Thread current) {
/*
* 如果是锁降级的话,步骤1的判断会失败,会往后面继续走,然后readerShouldBlock()会失败,因为一个已经获取
* 写锁的线程降级,那么他是没有前驱的,因此会直接走到第2和3的逻辑,如果读锁没有超限,就直接CAS设置state了
*/
HoldCounter rh = null;
for (;;) {
int c = getState();
//1.如果写锁不为0,并且被其他线程持有,返回-1
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)// 计数为 0 ,说明没得到读锁,清空线程变量
readHolds.remove();
}
}
if (rh.count == 0)// 说明没得到读锁
return -1;
}
}
//2.读锁超出最大范围抛出异常
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//3.CAS设置读锁成功
if (compareAndSetState(c, c + SHARED_UNIT)) {
//c=0,说明是第1次获取“读取锁”,则更新firstReader和firstReaderHoldCount
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
//如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程,则将firstReaderHoldCount+1
} else if (firstReader == current) {
firstReaderHoldCount++;
//里面需要处理之前获取锁的最后一个线程是不是自己的逻辑,和tryAcquireShared里面的逻辑很相似
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
//更新线程的获取“读取锁”的共享计数
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
四、小结
- 本文我们了解了读写锁的设计结构和思想,总体上是基于AQS来实现线程的同步和同步状态变量state的获取和释放的。因为读读共享,读写互斥,写写互斥,因此内部维持了2把锁。
- 因为内部有读锁和写锁,但是共享状态变量state只有一个,因此将其拆分为高16为和低16为,高位表示读锁状态,低位表示写锁状态,读锁和写锁都是可重入的。
- 内部的Sync继承自AQS并且实现了大部分操作state变量的方法,然后派生出两个子类分别对应公平非公平2种模式,对应的子类在初始化时会传递给读锁实例和写锁实例,读锁和写锁对应的api实际上内部是通过Sync这个实例来操作state变量的,读锁和写锁最大的不同在于state获取方式一个是共享式,一个是独占式。
- tryLock这个方法是无视公平原则的,它会快速尝试能不能获取到锁,即使在公平模式下他也可能会抢在其他线程的前面去获取锁。
- 读写锁是支持锁降级的,锁降级按照获取写锁->获取读锁->释放写锁的顺序在业务层面进行控制,因此在源码层面,持有写锁的线程是可以继续获得读锁的。