一、为什么要出现读写锁?
我们知道synchronizer 和 ReentrantLock 都是互斥锁
但现实很多业务场景都是读多写少,针对这种场景在并发中若采用 synchronizer 和
ReentrantLock 来保证原子性,但会降低代码的性能。这种场景,就可以使用读写锁
ReetrantReadWriteLock 来保证原子性,对于读操作使用读锁,对于写操作使用写锁,
读锁与读锁之间不是互斥的,读操作之间可以并发执行,但写锁与读锁之间、写锁与
写锁之间是互斥的,即只要涉及到了写操作也是需要互斥的。
ReetrantReadWriteLock 读写锁使用示例:
public class ReentrantReadWriteLockDemo {
static volatile int a=0;
public static void readA(){
System.out.println("a = "+a);
}
public static void writeA(){
a++;
}
public static void main(String[] args) {
/**
* 读写锁适用于读多写少的场景,读读并发,读写、写读、写写互斥,读读兼容
*/
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
Thread read1 = new Thread(()->{
readLock.lock();
try{
readA();
}finally {
readLock.unlock();
}
});
Thread read2 = new Thread(()->{
readLock.lock();
try{
readA();
}finally {
readLock.unlock();
}
});
Thread write1 = new Thread(() -> {
writeLock.lock();
try{
writeA();
}finally {
writeLock.unlock();
}
});
read1.start();
read2.start();
write1.start();
}
}
二、读写锁的实现原理
ReetrantReadWriteLock 也是基于AQS实现的,也是对AQS中的节点状态state进行操作,若
线程获取到了锁资源就去执行后边的业务,若没有获取锁资源就把当前线程包装成node,然
后将node放入AQS的阻塞队列中排队。
但有一点要注意,即:
读锁操作:基于state的高16位操作
写锁操作:基于state的低16位操作
2.1、ReetrantReadWriteLock锁重入的问题:
ReetrantReadWriteLock也是可重入锁
1)写锁重入:读写锁种的写锁重入方式与ReetrantLock锁的重入方式一样,都是对AQS
的state加1操作,只要确认持有锁的线程是当前写锁线程即可;区别是前
面ReetrantLock的重入次数是state的整数,而写锁的重入次数是state的低
16位,重入次数范围变小了。
2)读锁重入:因为读锁是共享锁,读锁在获取锁资源操作时,需要对AQS的state的高16
位进行+1操作;因为读锁允许统一时刻多个线程同时持有读锁,这样当多
个读操作在持有读锁时,无法确认每个线程持有读锁的重入次数;为了记
录读锁的重入次数,每个线程都有一个ThreadLocal 来记录其读锁的重入
次数。
3)写锁的饥饿问题:读锁是共享锁,当有现成持有读锁资源时,再来一个线程想获取读
锁,直接对state修改即可;在读锁资源先被占用后,来了一个写锁资源,
此时,当大量需要获取读锁的线程来请求锁资源时,如果可以绕过写锁直
接获取读锁资源,会造成写锁长时间无法拿到写锁资源。
读锁拿到锁资源后,如果再有读锁线程来获取锁资源,则需要去AQS阻塞
队列排队,如果队列的前边有需要获取写锁的线程节点,那么后续读锁线
程是无法拿到锁资源的,持有读锁的线程只会让写锁线程前边的读锁线程
拿到锁资源。
2.2、ReetrantReadWriteLock内部类Sync核心属性解析
ReetrantReadWriteLock.Sync 核心属性如下:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
/*
*
* 读vs写计数提取常量和函数。
* 锁状态逻辑上分为两个unsigned short:
* 下一个代表exclusive (writer)锁持有计数,上一个代表shared (reader)锁持有计数。
*
* 读锁和写锁都有锁重入
*
* todo 注意:
* AQS 的状态state是32位(int 类型)的,辦成两份,
* 读锁用高16位,表示持有读锁的线程数(sharedCount),
* 写锁低16位,表示写锁的重入次数 (exclusiveCount)。
* 状态值为 0 表示锁空闲,
* sharedCount不为 0 表示分配了读锁,
* exclusiveCount 不为 0 表示分配了写锁,
* sharedCount和exclusiveCount 一般不会同时不为 0,只有当线程占用了写锁,该线程可以重入获取读锁,反之不成立
*/
/**
* 读锁可以多个线程持有,而写锁只能有一个线程持有,所以称读锁--共享锁,写锁--互斥锁(排它锁),
* 我们在AQS中了解到,使用32位的全局变量state来保存锁的重入次数,这里我们将state分割
* 为高16位和低16位,其中高16位用来表示读锁
*/
//位数
static final int SHARED_SHIFT = 16;
//由于读锁使用高位,所以最大值加1,其实是AQS状态值加2^26
static final int SHARED_UNIT = (1 << SHARED_SHIFT);//共享锁,2^16 ,对读锁进行操作
//写锁的可重入最大次数、读锁的最大数量
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;//2^16 -1
//写锁的掩码,用于state的低16的值
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//2^16 -1
/*
* 读锁计数器,当前持有读锁的线程数
* 返回同步状态的高位16位的数值
* 读锁的个数或重入数
* >>>:二进制运算符,表示右移
*/
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/*
* 写锁计数器,写锁的重入次数,即写锁的线程数量
/
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
/**
* 内部类
* 一个用于记录每个线程持有读锁的计数器。存放在ThreadLocal中;
* 每个线程应用读写锁的线程都有一个HoldCounter对象
*
* todo 问题 :若想知道每个线程对于读锁的重入次数,该如何做?
* 使用ThreadLocal来进行统计,每个线程统计自己的
*/
/**
* A counter for per-thread read hold counts.
* Maintained as a ThreadLocal; cached in cachedHoldCounter
*/
static final class HoldCounter {
int count = 0;
//这个值是常量,使用id而不是线程的引用,是为了避免产生大量的垃圾
// 所使用线程的ID,初始值是当前线程的ID
final long tid = getThreadId(Thread.currentThread());
}
/**
* 内部类
* ThreadLocal 的子类,用来缓存每个线程的 HoldCounter;
* 采用继承是为了重写 initialValue() 方法,若 ThreadLocal 中没有
* 当前线程的计数,则 new 一个,这样就可以直接调用 ThreadLocal.get()
*/
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
/*
* 保存当前线程重入读锁次数的容器,当次数为0时移除
* 创建ThreadLocal对象
*/
private transient ThreadLocalHoldCounter readHolds;
/**
*
*
* 最近一个成功获取读锁的线程的计数器,这节省了ThreadLocal查找,通常情况下下一个要释放的线程是最后一个要获取线程,
* 这不是volatile 的,因为这只是一个试探的动作,线程缓存也是可以的(因为判断是否是当前线程是通过线程id来判断的)
*/
/**
* 缓存最后一个成功获取读锁的线程的重入次数,有两方面的好处:
* 1、避免了通过访问ThreadLocal来获取读锁的信息,这个优化的前提是
* 假设多数情况下,一个获取读锁的线程,使用完以后就会释放读锁,
* 也就是说最后获取读锁的线程和最先释放读锁的线程大多数情况下是同一个线程
* 2、因为ThreadLocal中的key是一个弱引用类型,当有一个变量持有HoldCounter对象时,
* ThreadLocalHolderCounter中最后一个获取锁的线程信息不会被GC回收掉
*/
private transient HoldCounter cachedHoldCounter;//保存最后一个线程的读锁数量
/**
* firstReader是第一个获得读锁的线程。
* firstReaderHoldCount是firstReader的保持计数。
* 更准确地说,firstReader是最后一次将共享计数从0更改为1的惟一线程,此后一直没有释放读锁;
* 如果没有这样的线程,则为空。
* 不能导致垃圾保留,除非线程终止而不放弃其读锁,因为tryreleasshared将其设置为null。
* 通过良性数据竞争访问;依赖于内存模型对引用的out- thin-air保证。
* 这使得跟踪非争用读锁的读持有非常便宜。
*
* firstReader 是一个特殊线程,他是最后一个 将共享计数从0更改为1的线程(在锁空闲的时候)
* firstReaderHoldCount 是 firstReader 的可重入计数
* firstReader 不能保留垃圾,所以在 tryreleasshared 里将其设置为null,除非线程是移除终止,没有
* 释放读锁
* firstReader 的作用是 跟踪无竞争的读锁计数器时非常便宜
*/
/**
* 第一个获取读锁的线程,有两方面的考虑:
* 1、记录将共享数量从0变成1的线程
* 2、对于无竞争的读锁来说进行线程重入次数数据的追踪的成本是比较低的
*/
private transient Thread firstReader = null;//保存获取到该锁的第一个读锁线程
//第一个获取读锁线程的重入次数
private transient int firstReaderHoldCount;//保存该锁第一个线程获取的读锁数量
Sync() {
readHolds = new ThreadLocalHoldCounter();
//若一个Java文件中同时包含volatile和非volatile 的属性,修改 volatile 修饰的属性时,非volatile
//修饰的属性也会立即对其他线程可见,等价于volatile 修饰,即保证可见性
setState(getState()); // ensures visibility of readHolds 确保readholds的可见性
}
}
三、写锁分析
3.1、写锁加锁流程概述
3.2、写锁加锁流程分析
3.2.1、lock() 方法(写锁的lock() 方法)
该方法是写锁加锁的入口,在该方法中调用Sync 的 acquire() 的方法来实现加锁的;
而Sync 是AQS的子类(acquire() 就是AQS中的方法,请参考前边AQS笔记)。
在AQS的 acquire() 方法中我们重点关注的是方法 tryAcquire在各个子类中的实现,
这里我们需要关注 tryAcquire在 ReetrantReadWriteLock.Sync 类中实现,acquire()
方法中调用的其他方法请参考AQS的笔记。
lock() 方法代码:
3.2.2、tryAcquire(int arg) 方法
该方法是 ReetrantReadWriteLock.Sync 中实现AQS中的方法,其功能是获取写锁
tryAcquire 中的方法 writerShouldBlock区分了写锁的公平锁与非公平锁的不同之处。
tryAcquire 方法代码如下:
/**
* 获取写锁:该方法由AQS调用,用于判定其子类的上锁逻辑;逻辑和原有获取互斥锁保持一致
*/
protected final boolean tryAcquire(int acquires) {
/*
* 有下面条件之一则获取锁失败:
* 1、如果读计数器不为0或写计数器不为0 ,或持有锁的线程不是同一个线程(即不是当前线程)则获取锁失败
* 2、如果计数器达到上限,则获取锁失败(这只能发生在计数器已经非0的情况)
* 3、其他的,这个线程有资格获取锁,如果该锁是可重入的或符合队列策略,获得锁后需要更新持有锁的
* 同步状态
*/
//获取当前线程
Thread current = Thread.currentThread();
//获取当前同步状态,即锁的状态 0=表示没有线程持有该锁
//获取当前状态值和互斥锁的数量
int c = getState();
//返回写锁的数量,互斥锁一般是写锁
int w = exclusiveCount(c);//获取state低16位的值
if (c != 0) {//表示有线程持有该锁
//锁资源已经被读锁独占 或 正在持有写锁的线程不是当前线程,则获取锁失败
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || //w =0 表示没有线程持有写锁,
current != getExclusiveOwnerThread())
//获取锁失败,让AQS对当前线程执行阻塞操作,即将当前线程放入阻塞队列
return false;
//代码走到这里表示当前线程已经持有写锁
//写锁重入次数超过上限 MAX_COUNT,则抛出异常
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)) //通过CAS抢写锁
//抢写锁失败
return false;
//若当前线程获取了锁,则设置持有锁的线程
setExclusiveOwnerThread(current);//抢写锁成功,则将当前线程标识为获取互斥锁的线程对象
return true;
}
/**
* 公平锁下,writerShouldBlock和readerShouldBlock方法都表示当有别的线程也在尝试获取锁时,是否应该阻塞。
* 对于公平模式,hasQueuedPredecessors()方法表示前面是否有等待线程。一旦前面有等待线程,那么为了遵循公平,
* 当前线程也就应该被挂起。
*
* 这里区分公平锁 与 非公共平锁的不同之处
* 公平锁 ReentrantReadWriteLock.FairSync 类中的方法
* @return
*/
final boolean writerShouldBlock() {
/**
* 判断AQS中的阻塞队列中是否有其他线程正在等待唤醒,若有,则当前线程需要进入阻塞队列中排队
*/
return hasQueuedPredecessors();
}
/**
* 非公平模式下,writerShouldBlock直接返回false,说明不需要阻塞
*
* 非公平锁 ReentrantReadWriteLock.NonFairSync 类中的方法
* @return
*/
final boolean writerShouldBlock() {
//读写锁的本身就是为了解决读多写少的场景,此时不应该让写锁饥饿
//而且非公平锁的写锁永远不阻塞,可以直接去竞争锁
return false; // writers can always barge
}
3.3、写锁释放锁流程分析
写锁释放锁的入口是unlock() 方法,在unlock() 方法中调用ReetrantReadWriteLock.Sync的
release()方法,即AQS的release方法,如下图所示:
在release()方法中我们重点关注 tryRelease()方法,tryRelease方法由AQS定义,但在AQS
的各个子类中实现,这里我们需要关注在AQS子类 ReetrantReadWriteLock.Sync 中的实现
tryRelease() 在 ReetrantReadWriteLock.Sync 代码如下
//独占式下释放锁,即释放写锁
protected final boolean tryRelease(int releases) {
/**
* 判断是否获取到了写锁,若没有写锁则抛出异常
* 没有获取写锁,为啥需要释放写锁呢?
*/
//判断持有写锁的线程是否是当前线程
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//获取state
int nextc = getState() - releases;
//线程持有锁的个数是否为0,为0表示没有线程持有锁,即锁已经释放了
//释放完毕后,写锁状态为0
/**
* 写锁释放为0,判断写锁计数器是否为0,为0表示写锁释放成功
* 为什么要判断是否为0?
* 1、锁的重入,不为0表示有锁的重入
* 2、当前线程释放写锁,由于非公平锁的原因,B线程立即持有写锁,这时写锁的个数又大于0了
* 这里考虑的是锁重入
*/
boolean free = exclusiveCount(nextc) == 0;
//若nextc 等于0,表明当前线程完全释放了锁,也即锁重入为0,将当前线程从ownerThread对象中移除
if (free)
setExclusiveOwnerThread(null);
//设置同步状态,即设置全局的state变量的值
setState(nextc);
//返回锁是否释放成功,若返回True,则表示释放锁成功,由AQS完成后面阻塞队列中的线程的唤醒
return free;
}
四、读锁分析
4.1、读锁加锁流程概述
4.2、读锁加锁流程分析
4.2.1、lock()(读锁 ReadLock 的lock方法)
该方法是读锁加锁的入口,在该方法中调用ReetrantReadWriteLock.Sync 的
acquireShared(),Sync是AQS的子类,acquireShared 方法其实是AQS的方法。
如下图所示:
在 acquireShared 方法中这里我们只需要方法 tryAcquireShared,其他方法请参照前边笔
记AQS(二)。
方法 tryAcquireShared在AQS中定义,但在AQS的各个子类中实现,这里我们需要关注
子类 ReetrantReadWriteLock.Sync 中的实现
4.2.2、tryAcquireShared(int unused) 方法
该方法功能是获取读锁
tryAcquireShared 中方法 readerShouldBlock() 区分了读锁的公平锁与非公平锁的不同
的地方,
readerShouldBlock() 方法功能是判断读锁线程是否应该被阻塞,其在类
ReetrantReadWriteLock 定义,分别在 内部类 ReetrantReadWriteLock.FairSync 和
ReetrantReadWriteLock.NonFairSync 中实现。
tryAcquireShared方法代码如下:
protected final int tryAcquireShared(int unused) {
/*
*
* 下面条件之一,则获取读锁失败:
* 1、如果写锁被另外一个线程持有,则获取失败
* 2、另外,如果这个线程可以获取锁则进入WRT状态,并根据阻塞队列(即公平锁)策略判断该线程是否需要阻塞。
* 如果不需要阻塞,则通过CAS来更新同步状态(即获取锁)、更新读/写锁计数器,则获取读锁成功。
* 注意这一步没有检查 “可重入获得”(即检查锁是否可重入),可重入检查被推迟到完整版本,
* 以避免在更典型的不可重入情况下必须检查hold count 读/写 锁计数器。
* 3、如果步骤2失败,则进入fullTryAcquireShared 方法中尝试获取读锁
*
* 1)有线程持有写锁,且该线程不是当前线程,获取读锁失败。
* 2)写锁空闲 且 公平锁策略决定 读线程应当被阻塞,除了重入获取,其他获取锁失败。
* 3)读锁数量达到最多,抛出异常。
* 除了以上三种情况,该线程会循环尝试获取读锁直到成功。
*/
//获取当前线程
Thread current = Thread.currentThread();
//获取锁的同步状态值
int c = getState();
// 锁资源已经被写锁独占,且 当前持有锁的线程不是当前线程,则返回-1,告诉AQS获取共享锁失败
if (exclusiveCount(c) != 0 && //有没有线程持有写锁
getExclusiveOwnerThread() != current) //有线程持有写锁,则继续判断持有写锁的线程是否是当前线程,若是则返回-1,告诉AQS获取共享锁失败
return -1;
//获取读锁计数,持有读锁的线程数
int r = sharedCount(c);
/**
* 1、读锁不需要等待
* 2、读锁未超过上限
* 3、设置读锁的state值成功
* 则返回成功
*/
if (!readerShouldBlock() &&//让子类来判定获取读锁的线程是否应该被阻塞
r < MAX_COUNT &&//读锁未超过上限,即释放发生了溢出
compareAndSetState(c, c + SHARED_UNIT)) {//CAS增加state高16位读锁的持有数量,即获取共享锁成功
//增加state高16位(读锁数量)之前的计数器为0,表明当前线程就是第一个获取读锁的线程
if (r == 0) {
//使用2个线程来优化ThreadLocal
//记录第一个获取读锁的线程信
firstReader = current;//
//读锁第一次被获取,则读锁的重入数为 1
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//持有读锁的线程数不为0(即读锁已经有线程在持有),这时若firstReader为当前线程,
// 则这时当前线程算重入读锁, firstReader 计数器firstReaderHoldCount加1,
// 即第一个获取读锁的线程再次获取锁(重入)
firstReaderHoldCount++;
} else {
//如果读锁计数不等于0,firstReader也不是当前线程,此时将获取读取锁的次数保存在ThreadLocal中;
HoldCounter rh = cachedHoldCounter;
//如果 cachedHoldCounter 的值为null 或 最近一个获取读锁的线程不是当前线程
if (rh == null || rh.tid != getThreadId(current))
//以当前线程读锁的重入次数来更新 cachedHoldCounter
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)//表示最后一个线程持有的读锁数量为0,即此时没有线程持有读锁
//若 当前线程持有的读锁数量保存在ThreadLocal 中,即当前线程是最后一个持有读锁的线程
readHolds.set(rh);
//最后将读锁的重入次数加1
rh.count++;
}
return 1;
}
//todo 前面额判断是为了在前置优化 fullTryAcquireShared
//获取读锁失败,进入 fullTryAcquireShared 中继续尝试获取
return fullTryAcquireShared(current);
}
4.2.3、readerShouldBlock() 方法
该方法功能是判断读锁线程是否应该被阻塞;其在类 ReetrantReadWriteLock 定义,
分别在 内部类 ReetrantReadWriteLock.FairSync 和ReetrantReadWriteLock.NonFairSync
中实现。
readerShouldBlock 方法代码如下:
/**
在 FairSync 中的实现
*/
final boolean readerShouldBlock() {
return hasQueuedPredecessors();//看前面是否有读锁在排队
}
//判断前面是否有读锁在排队
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
//===============================================================================
/**
* 判断读锁是否需要阻塞
*
* 解决写锁线程饥饿:
* 在获取读锁之前看下是否有写锁线程在阻塞排队
*
* 在 NonFairSync 中的实现
*/
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();//看队列中第一个等待的线程是否是互斥锁,即写锁
}
}
//看队列中第一个等待的线程是否是互斥锁,即写锁
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
4.2.4、fullTryAcquireShared(Thread current) 方法
该方法功能是通过自旋的方式不停的尝试获取读锁
fullTryAcquireShared 方法代码如下:
final int fullTryAcquireShared(Thread current) {
/*
*
* 调用该方法的线程都是希望获取读锁的线程,有3种情况:
* 1、在尝试通过CAS操作修改state时由于有多个竞争读锁的线程导致CAS操作失败
* 2、需要排队等待获取读锁的线程(公平锁)
* 3、超过读锁限制的最大申请次数的线程
*/
HoldCounter rh = null;//当前线程获取读锁的数量
//自旋,获取读锁
for (;;) {
//获取锁状态
int c = getState();
//若资源已经被写锁独占了,且持有写锁的线程不是指定的线程 current,则直接返回获取锁失败
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current) //锁资源被其他写锁线程占用
return -1;
// else we hold the exclusive lock; blocking here 否则我们持有互斥锁;阻塞会导致死锁。
// would cause deadlock.
} else if (readerShouldBlock()) {//子类判断当前读线程是否需要阻塞,若需要阻塞则进入判断
// 确保我们不是以可重入的方式获取读锁
if (firstReader == current) {//若 firstReader 是当前线程,则不做任何处理
// assert firstReaderHoldCount > 0;
} else {
//获取当前线程记录读锁重入次数的 HoldCount对象
//不是第一个读线程,则对ThreadLocal 操作
// 清理当前线程中重入次数为0的数据
if (rh == null) {//这个判断是为了获取当前线程读锁的重入次数
//最近一个获取读锁的计数器
rh = cachedHoldCounter;
//若最近获取读锁的线程不是当前线程current,且读锁的重入次数为0,则删除
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)//重入次数为0则删除
readHolds.remove();
}
}
if (rh.count == 0)//若当前读锁的重入次数为0,则表示没有获取到阻塞,返回-1,阻塞当前线程
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)//若读锁的个数到达了上限 MAX_COUNT,则抛出异常,获取锁失败
throw new Error("Maximum lock count exceeded");
//CAS增加读锁次数,即获取读锁,若执行失败,则进入下一次循环
if (compareAndSetState(c, c + SHARED_UNIT)) {
//表示当前线程是第一个获取到读锁的线程
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {//判断第一个线程是否是当前线程
//当前线程重复获取读锁(锁重入)
firstReaderHoldCount++;
} else {//操作ThreadLocal
// 在readHolds中记录获取锁的线程的信息
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;
}
//获取锁成功
return 1;
}
}
}
4.3、读锁释放锁流程分析
读锁释放锁的入口是方法 unlock(),在 unlock 中调用ReetrantReadWriteLock.Sync的
releaseShared方法去释放锁(其实是AQS的方法),如下图所示:
在 releaseShared 方法中我们只需要关注 tryReleaseShared 方法,该方法在AQS中定义,
由其的各个子类实现,releaseShared 的其他方法请参考前边的AQS(二) 笔记
在这里,我们需要看下 releaseShared 方法在 ReetrantReadWriteLock.Sync 子类中的实现
ReetrantReadWriteLock.Sync 中的方法 releaseShared 的代码如下:
/**
* 释放读锁,需要考虑:
* 重入多少次,就要减多少次
* 先完成自己线程的读锁的释放,然后再CAS完成state高16位读锁的释放
*/
protected final boolean tryReleaseShared(int unused) {
//获取当前线程
Thread current = Thread.currentThread();
/**
* 如果当前线程是第一个获取读锁的线程,有两种情况:
* 1、如果持有锁的次数为1,直接释放成功
* 2、如果持有锁的次数大于1,说明有重入的情况,需要次数减1
*/
if (firstReader == current) {//当前线程是第一个获取到读锁的线程
//若 当前线程持有锁的个数firstReaderHoldCount等于1,表示可以直接释放读锁,
//否则,表示读锁有重入,firstReaderHoldCount自减1
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;//表示读锁有重入
} else {//第一次持有锁的线程不是当前线程
//获取线程持有锁的计数器
HoldCounter rh = cachedHoldCounter;
//若最后持有读锁的计数器为null 或 最后一个持有读锁的线程不是当前线程,
//则获取当前线程对应的 HoldCounter
if (rh == null || rh.tid != getThreadId(current))
//则获取当前线程对应的 HoldCounter 计数器,即读锁的个数
rh = readHolds.get();
//获取当前线程重入锁的次数
int count = rh.count;
//若当前线程只持有一次读锁(即没有重入),则删除 readHolds 中的数据
//若当前线程没有持有锁,则抛出异常
if (count <= 1) {//小于1,表示当前线程已经释放完了读锁,不需要在ThreadLocal中持有HolderCount对象
readHolds.remove();
if (count <= 0)//非法锁状态
throw unmatchedUnlockException();
}
//计数器减1
--rh.count;
}
//减共享状态state,即CAS释放state高16位的读锁
//自旋,释放锁,并判断当前线程释放锁后,锁是否是空闲的(即nextc==0)
for (;;) {
int c = getState();
// 如果是最后一个释放读锁的线程nextc为0,否则不是
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))//释放锁
//释放完成后判断是否为0,若为0 则表示所有的读锁都释放了,此时是无锁状态;
// 那么此时需要干些啥?释放完成后需要由AQS来唤醒后面的阻塞的线程
return nextc == 0;
}
}