前几篇文章分别介绍了AQS的基本的加锁解锁流程,Condition,CountDownLatch共享锁等。这篇文章继续介绍关于ReentrantReadWriteLock相关的原理。
从名字来看就知道ReentrantReadWriteLock是读写锁,读锁指的是共享锁,而写锁则是独占锁。在前几篇文章中了解到ReentrantLock内部通过AQS来实现的是独占锁加锁,其中利用了state变量的值来实现是否加锁的操作。而CountDownLatch则是通过state值是否减为0来实现共享锁。那么ReentrantReadWriteLock却可以既实现共享锁又实现独占锁,它具体是怎么实现的呢?,下面就一步一步解析一下源码。
ReentrantReadWriteLock的类结构
注:下图中绿色箭头代表 implements 实现
紫色箭头代表 extends 继承
红色带’+'号的 代表 内部类
1、从图中,我们可以看到 ReentrantReadWriteLock 实现了 ReadWriteLock 接口。其中只有两个方法如下:
// ReadWriteLock 只有两个方法,一个获取读锁,一个获取写锁
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
2、ReadLock和WriteLock作为ReentrantReadWriteLock 内部类 都实现了Lock接口中的lock和tryLock等方法。
3、FairSync和NonFairSync作为ReentrantReadWriteLock 内部类 继承自Sync类,分别实现了公平锁和非公平锁。
这里和ReentrantLock实现不太一样。
在ReentrantLock中,FairSync和NonFairSync都有自己各自的实现方式。都有自己的实际的方法体。
而在ReentrantReadWriteLock 中则将主要实现过程都放在了Sync类中,FairSync和NonFairSync中只是简单的判断了是否为阻塞方式进行(判断是否需要进行排队),需要注意的是,读写锁都可以支持公平和非公平两种模式。
源码解析
首先要解决开始的疑问。
同时实现共享和独占锁 ?
通常state的含义在不同模式下表示方式也不同
独占模式:0 代表未获取锁,1代表获取锁。
共享模式:每个线程都可以获取锁,对state进行加减操作。
为了能够兼容共享和独占模式,jdk将state这个int类型的值(4个字节,32位)分为了高16位和低16位。其中高16位用于共享模式锁的获取次数,低16位用于独占模式锁的重入次数。
好了,具体的细节,怎么操作的下面跟着源码一步一步来看吧。
ReadLock
我们源码都从构造方法和方法调用入口:
//1、默认非公平锁实现模式
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//获取读锁
Lock readLock = readWriteLock.readLock();
//加读锁
readLock.lock();
//释放读锁
readLock.unlock();
构造方法:
//读写锁的内部类变量声明
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
//1、默认构造方法,非公平锁
public ReentrantReadWriteLock() {
//这里去调用带参数的构造方法 ReentrantReadWriteLock(boolean fair)
this(false);
}
//带参数的构造方法,方法会默认去实例化读写锁
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
//调用writeLock返回写锁
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
//调用readLock返回读锁
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
读锁加锁过程:
//加锁
public void lock() {
//共享模式加锁,默认sync为NonFairSync
sync.acquireShared(1);
}
下面开始加锁流程的实现了,提前说明一下,在AQS共享锁中,tryAcquireShared(arg)返回结果如果<0 代表没有获取共享锁,如果>0则代表获取到了共享锁。
这个其实跟CountDownLatch是一样的。
//这里很熟悉吧,去调用AQS的方法了
public final void acquireShared(int arg) {
//这里就调用ReentrantReadWriteLock
//重写的tryAcquireShared的方法了
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
接下来tryAcquireShared 方法的实现和CountDownLatch不一样。重点看这个方法。
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//记录最后一个获取读锁的线程计数
private transient HoldCounter cachedHoldCounter;
//通过threadLocal设置每个线程获取读锁的计数
private transient ThreadLocalHoldCounter readHolds;
//默认初始化一个HoldCounter
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
//好了来了要真正加锁的方法了,
//其实基本流程和之前共享锁流程都差不多
//只是在state的操作上有些区别,又额外加了一些多线程的属性
protected final int tryAcquireShared(int unused) {
/*
获取当前线程
*/
Thread current = Thread.currentThread();
//这里获取的state为一个32位的二进制数
int c = getState();
//如果有线程拥有该独占锁并且独占锁不是当前线程
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
//加锁失败
return -1;
//获取当前共享锁持有锁的线程数
int r = sharedCount(c);
//1、readerShouldBlock()判断是否是公平锁默认是非公平
//2、r < MAX_COUNT是否已经达到了共享锁限制的最大数量
// MAX_COUNT = 2^16 -1 ;
//3、CAS直接更新state值尝试获取锁
if (!readerShouldBlock() &&
r < MAX_COUNT &&
//这里c+SHARED_UNIT 是因为共享锁是高16位,那么
//在给共享锁获取次数赋值的时候,需要向左移动16位
//这里是高16位+1
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
//记录第一个获取读锁的线程
//当firstReader释放锁后,
//又会有新的线程来占有锁
//所以firstReader是不断更新的
firstReader = current;
//记录第一个获取读锁的重入次数
//这里应该是为了更方便记录锁重入次数吧
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//锁重入
firstReaderHoldCount++;
} else {
/**
1、HoldCounter 这里记录了每个线程持有锁的计数,
是以 ThreadLocal方式来进行缓存的
主要是通过readHolds.set()来设置的
2、cachedHoldCounter记录是最后一个
获取读锁的线程计数
3、判断如果cachedHoldCounter记录的不是当前线程
则直接设置为当前线程 **/
if (rh == null || rh.tid !=
getThreadId(current))
cachedHoldCounter = rh =
readHolds.get();
else if (rh.count == 0)
//cachedHoldCounter 设置为当前线程计数后,count还是0
//设置threadLocal readHolds,
//readHolds默认实例化一个新的HoldCounter
readHolds.set(rh);
//count数加一
rh.count++;
}
//获取锁成功
return 1;
}
//来到这里说明上面的条件不符合,要继续去尝试获取锁
return fullTryAcquireShared(current);
}
这里简单解释一下获取独占锁加锁次数为什么是c & EXCLUSIVE_MASK;
1、首先EXCLUSIVE_MASK= (1 << SHARED_SHIFT) - 1这就意味着是长达15位的全是1的一个掩码值(例如4的二进制位100,也就是2^2,
那么 2^2 -1 = 3 = 111)
2、其次独占锁是低16位,而c值是一个32为位的二进制数,这里c & EXCLUSIVE_MASK 执行的是一个与操作,1 << 16 -1 为 15位那么掩码不够32位的前面要用0来补齐。
而与操作是只有两个都是1才会为1,所以前面补齐的位 & 之后都是0,只有独占锁低位15位上面,如果已经有线程获取独占锁了,那么与结果才为1,有几次加锁次数,结果就是几。通过此方法来获取独占锁获取次数。
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
//获取独占锁加锁次数(包含重入次数)
static int exclusiveCount(int c) {
return c & EXCLUSIVE_MASK;
}
//持有共享锁线程数量
static final int SHARED_SHIFT = 16;
static int sharedCount(int c) {
//高位代表共享锁持有数量,c向右移16位
//代表真正获取共享锁的次数
return c >>> SHARED_SHIFT;
}
好了,接下来继续尝试获取读锁。
final int fullTryAcquireShared(Thread current) {
/*
tryAcquireShared方法中CAS失败,这里要自旋去获取锁
*/
HoldCounter rh = null;
for (;;) {
int c = getState();
//判断如果有线程持有写锁
if (exclusiveCount(c) != 0) {
//并且写锁不是当前线程
if (getExclusiveOwnerThread()
!= current)
//直接获取失败,返回进入阻塞队列中
return -1;
/**else{}
这里省略了else 就是当前线程已经持有写锁
那么就直接继续向下执行
这里涉及到了锁降级处理,如果还是同一个线程
既获取了写锁,又要获取读锁,则直接降级为读锁
否则如果阻塞的话就会造成死锁
**/
//这里涉及到了阻塞问题,公平锁和非公平的问题下面说
} else if (readerShouldBlock()) {
//进入到这里的,都是需要排队的
//但是如果是线程锁重入则不需要排队
//所以下面这段代码主要是判断是否为锁重入的
//线程重入读锁,直接到下面的CAS获取锁重入代码
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
//不是锁重入,那么就是其他新的线程
if (rh == null) {
//rh先设置为之前最后一个获取锁的线程计数
rh = cachedHoldCounter;
//如果为null或者不是当前线程
if (rh == null || rh.tid != getThreadId(current)) {
//rh重新赋值为一个新的线程变量信息
rh = readHolds.get();
//如果是刚刚初始化的,count = 0
if (rh.count == 0)
//那么直接移除掉
readHolds.remove();
}
}
//最后rh不为null时,
//如果count =0 则跳出循环加入阻塞队列中
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//尝试去获取读锁,或者是重入读锁
if (compareAndSetState(c, c + SHARED_UNIT)) {
//因为进入到这就说明已经获取读锁了,state不可能为0了
//????这个条件我一直没有理解,总觉得不会执行到这里
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
//再次判断firstReader是否为当前线程
} else if (firstReader == current) {
//如果是的话,更新firstReaderHoldCount
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
//新线程进入获取锁时,count = 0
//当线程来获取锁时,则直接赋值了
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh;
}
return 1;
}
//这里省略了else,else就是CAS失败,会去继续自旋获取锁
}
}
当fullTryAcquireShared方法结束后仍然返回 -1加锁失败后,那么就会进入阻塞队列中去排队获取锁了。
读锁的加锁流程图:
读锁的释放
//读锁解锁
readLock.unlock();
public void unlock() {
//跟之前的思路一样
this.sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
//如果成功释放共享锁
if (this.tryReleaseShared(arg)) {
//唤醒等待的节点
this.doReleaseShared();
return true;
} else {
return false;
}
}
接下来看读锁释放的具体实现:
//尝试去释放读锁
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//释放锁之前,要先更新一下firstReader
//和cachedHoldCounter的记录的值
if (firstReader == current) {
// 如果记录的第一个获取读锁的线程是当前线程
//并且获取锁次数为1
if (firstReaderHoldCount == 1)
//那么释放锁之后,这个要设置为null
firstReader = null;
else
//否则 -1
firstReaderHoldCount--;
} else {
//如果不是第一个获取锁的,那么就可能是最后一个
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
//如果不是最后一个,要从threadLocal中去获取
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
//如果<=1则直接移除,防止内存泄漏
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
//如果count>1则每次释放锁 -1
--rh.count;
}
//上面完成了线程计数器的更新,接下来要完成state的更新了
for (;;) {
int c = getState();
//释放锁每次state - 1
int nextc = c - SHARED_UNIT;
//更新state值
if (compareAndSetState(c, nextc))
//如果为0则释放完成,否则返回false
return nextc == 0;
}
}
WriteLock
写锁的加锁过程
public static ReadWriteLock readWriteLock =
new ReentrantReadWriteLock();
//获取写锁
public static Lock writeLock = readWriteLock.writeLock();
//对写锁加锁
writeLock.lock();
//默认非公平锁
public void lock() {
sync.acquire(1);
}
//这个方法和ReentrantLock一样
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//读写锁中关于写锁加锁的实现
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();
//获取独占锁数量
int w = exclusiveCount(c);
//c !=0 说明有线程持有锁,可能为读锁,也可能为写锁
if (c != 0) {
//如果写锁为0(那么有线程持有读锁),
//或者写锁线程不是当前线程
if (w == 0 || current != getExclusiveOwnerThread())
//直接返回false,说明读写锁不能兼容
return false;
//如果写锁数量超出范围,直接抛出异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//到这里,说明没有线程获取读锁,
//并且是当前线程已经拥有了写锁,相当于写锁重入
setState(c + acquires);
//返回加锁成功
return true;
}
//走到这里,说明既没有线程获取读锁也没有线程获取写锁
//如果需要阻塞,或者尝试cas失败
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
//直接返回去阻塞队列排队
return false;
//否则将持有写锁的线程设置为当前线程
setExclusiveOwnerThread(current);
//返回加锁成功
return true;
}
这之后如果tryAcquire返回false,那么会去执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg),这个方法和之前说过的一抹一样,如果还不太清楚的可以去翻看之前的博客。
写锁的加锁流程图:
写锁的释放
writeLock.unlock();
public void unlock() {
sync.release(1);
}
//这里释放和之前的释放相同
public final boolean release(int arg) {
//这个方法时需要重写的
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//如果释放成功,则唤醒h之后的节点
unparkSuccessor(h);
return true;
}
return false;
}
//重写释放锁的方法
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//每次释放state - 1
int nextc = getState() - releases;
//直到state == 0 释放成功
boolean free = exclusiveCount(nextc) == 0;
if (free)
//如果释放成功设置拥有锁线程为null
setExclusiveOwnerThread(null);
//将state设置为0
setState(nextc);
return free;
}
公平和非公平的问题
公平和非公平的问题,主要是看读写锁在判断是否需要排队时的条件是什么?
公平锁的情况下读锁和写锁的条件为:
static final class FairSync extends Sync {
//写锁公平条件下判断是否阻塞
final boolean writerShouldBlock() {
//直接判断是否有前驱节点在排队
return hasQueuedPredecessors();
}
//读锁公平条件下判断是否阻塞
final boolean readerShouldBlock() {
//直接判断是否有前驱节点在排队
return hasQueuedPredecessors();
}
}
也就是在公平锁情况下,读锁和写锁的判断条件都是阻塞队列中是否有前驱节点在排队。
非公平下:
static final class NonfairSync extends Sync {
//非公平下,写锁始终都不会阻塞
final boolean writerShouldBlock() {
return false; // writers can always barge
}
//非公平下,读锁也会去判断队列中头节点之后第一个节点是否写锁
//如果是写锁,会先阻塞读锁,让写锁先执行
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
可见,在读写锁实现中,给与了写锁更高的优先级。