【Java 并发】【九】【AQS】【八】ReentrantReadWriteLock之ReadLock读锁原理
1 前言
上节我们看了下ReentrantReadWriteLock读写锁的写锁的申请和释放过程,这节我们就来看下读锁的。
2 线程读锁记录
回顾一下之前的例子,在读写并发操作的时候,读取数据的时候加读锁:
public class ReentrantReadWriteLockTest { // 声明一个读写锁 private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // 声明写锁 private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock(); // 声明读锁 private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock(); // 共享变量 private static int value = 0; // 读取数据操作 public static int getValue() { try { // 读取前加读锁 readLock.lock(); return value; } finally { // 释放读锁 readLock.unlock(); } } public static void addValue() { try { writeLock.lock(); value++; } finally { writeLock.unlock(); } } }
上面的getValue方法就是使用读锁的样例。readLock.lock、readLock.unlock分别对应着读锁的加锁和释放,之前我们讲state 的高16位表示读锁个数,那现在问题来了,每个线程怎么知道自己读锁加了多少次?由于读锁是共享的,所以state变量上表示不出每个线程加读锁的个数。应该是每个线程都会记录自己加了多少个读锁;每个线程都有自己的一份记录,所以这里就用到了ThreadLocal。ReentrantReadWriteLock就是使用ThreadLocal来记录每个线程读锁的个数的。它具体的设计如下所示:
static final class HoldCounter { // 当前线程的读锁个数,count的初始值是0 int count = 0; // 当前线程的id final long tid = getThreadId(Thread.currentThread()); } // ThreadLocalHoldCounter 读锁存储容器,这里继承了ThreadLocal static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> { public HoldCounter initialValue() { return new HoldCounter(); } } // 这里本质是一个ThreadLocal,每个线程通过它可获取自己读锁的个数 private transient ThreadLocalHoldCounter readHolds;
我们接下来就分析下读锁的底层是怎么实现的。
3 读锁加锁lock源码分析
读锁加锁的入口方法源码如下:
public void lock() { //调用sync的acquireShared()方法,也还是进入了AQS的acquireShared方法了 sync.acquireShared(1); }
这里调用的是Sync同步器的acquireShared方法,最后还是进入了AQS的acquireShared方法:
public final void acquireShared(int arg) { // 1.调用子类Sync的tryAcquireShared方法 // 2. 如果获取读锁失败,则调用doAcquireShared进入等待队列等待 if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
获取读锁的入口acquireShared的模板流程:
(1)首先调用子类的tryAcquireShard方法去尝试获取读锁,也就是调用子类Sync的tryAcquireShared方法尝试获取读锁
(2)如果获取读锁成功,直接返回;否则获取失败,调用doAcquireShared方法进入等待队列等待
我们画个图理解一下:
doAcquireShared方法之前讲解AQS的时候已经分析过了,我们来看一下Sync的tryAcquireShared方法。
3.1 Sync的tryAcquireShred源码实现
protected final int tryAcquireShared(int unused) { // 获取当前线程 Thread current = Thread.currentThread(); // 获取state变量的值 int c = getState(); // 计算写锁的次数,如果写锁次数非0,且加写锁的不是自己 // 说明别人加了写锁,自己这时候获取读锁失败,返回-1 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; // 计算一下读锁的个数 int r = sharedCount(c); // 调用readerShouldBolck,判断是否是公平锁,是否允许加锁 if (!readerShouldBlock() && // 读锁个数r < 65535,说明读锁个数还剩余 r < MAX_COUNT && // 执行cas尝试加读锁 compareAndSetState(c, c + SHARED_UNIT)) { // 如果r == 0,说明自己是第一个加读锁的线程 if (r == 0) { // 记录一些第一个加读锁线程 firstReader = current; // 第一个加锁线程加锁次数为1 firstReaderHoldCount = 1; } // 如果自己是第一个加读锁的线程,说明之前加锁过了 // 直接修改一下次数即可 else if (firstReader == current) { firstReaderHoldCount++; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) // 从ThreadLocal中获取下当前线程加锁的次数 cachedHoldCounter = rh = readHolds.get(); // 如果当前线程第一次加锁,设置一下ThreadLocal else if (rh.count == 0) readHolds.set(rh); // 当前线程加锁次数加1即可 rh.count++; } return 1; } // 如果上面CAS操作加锁失败了,进入这个兜底方法 return fullTryAcquireShared(current); }
我们画个图来理解一下:
上面的流程图,我们再一步步分析一下:
(1)首先线程进来,先获取锁的记录变量state
(2)计算一下写锁个数,如果写锁个数非零,并且加写锁的线程不是自己,那么由于读写互斥,此时加读锁失败,返回-1
(3)如果写锁个数为零,或者是自己加了写锁,则继续
(4)readShouldBlock根据当前的公平锁、非公平锁、等待队列情况返回是否允许加锁,如果不允许则暂时获取锁失败,进入兜底加锁机制,即执行fullTryAcquireShared方法。
(5)判断读锁的加锁次数,r < MAX_COUNT即65535是否达到上限,如果达到上限则进入兜底加锁机制fullTryAcquireShared。
未达到上限则执行CAS操作尝试去加读锁,如果CAS加锁失败,也会进入兜底加锁机制fullTryAcquireShared方法
(6)如果CAS加锁成功了,则从ThreadLocal获取当前加读锁的次数,将加读锁的次数+1,就可以了
3.2 fullTryAcquireShared加读锁源码
那我们来看下兜底机制,里面到底是个什么逻辑。
final int fullTryAcquireShared(Thread current) { HoldCounter rh = null; // 在for循环中,不断重试,知道有结果 for (;;) { // 获取读写锁的变量state int c = getState(); // 如果有写锁,并且加锁不是自己 // 说明别人加了写锁,读写互斥,直接返回失败 if (exclusiveCount(c) != 0) { if (getExclusiveOwnerThread() != current) return -1; // 根据公平、非公平模式、等待队列等判断是否应该被阻塞 } else if (readerShouldBlock()) { // 如果被阻塞 // 第一个加读锁的线程是自己,啥也不干 if (firstReader == current) { // assert firstReaderHoldCount > 0; } else { if (rh == null) { // 自己不是第一个加读锁的线程 // 则获取一下自己加读锁的次数 rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) { rh = readHolds.get(); // 如果加读锁的次数是0,从ThreadLocal从移除 if (rh.count == 0) readHolds.remove(); } } // 加读锁次数是0此,此时有应该阻塞,直接返回加锁失败 if (rh.count == 0) return -1; } } // 如果读锁次数已达上限,抛出异常 if (sharedCount(c) == MAX_COUNT) throw new Error("Maximum lock count exceeded"); // cas操作尝试加锁,如果cas加锁成功,进入下面的逻辑 if (compareAndSetState(c, c + SHARED_UNIT)) { // 如果自己是第一个加锁的线程,设置一下第一个加锁的人是当前线程 if (sharedCount(c) == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { // 第一个加锁的线程是自己,将自己加锁次数+1即可 firstReaderHoldCount++; } else { // 这里的操作不外乎就是从ThreadLocal从取出自己加锁的次数,然后将次数+1即可 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 } // 加锁成功,返回1 return 1; } } }
其实大体上跟tryAcquireShred差不多的,只是在一个for循环里面不断重试而已,提高加锁成功的概率,下面我们也是再画个图来看一下:
上面的流程图,我们再捋一下:
(1)首先获取读写锁状态state,判断如果有别人加了写锁,由于读写互斥,则直接加读锁失败,返回-1
(2)如果别人没有加写锁,判断一下自己是否应该被阻塞(结合公平锁、非公平所、等待队列)。
如果应该被阻塞,且自己加读锁的次数count == 0,则返回-1,加锁失败。
如果自己是第一个加读锁的线程,可以网开一面,继续尝试获取读锁,进入for循环重试
(3)如果不应该被阻塞,判断加锁次数是否达到上限,如果达到上限,直接抛出异常
(4)如果读锁次数还有剩余,直接CAS操作尝试加锁,加锁失败则进入for循环重试。
如果加锁成功,则从ThreadLocal中取出之前加锁次数,然后将加锁次数+1,最后返回1,表示本次操作加锁成功。
4 读锁释放锁unlock源码分析
我们接下来继续,讲解读锁ReadLock的释放锁流程:
public void unlock() { // 调用的还是Sync同步器的releaseShared方法,也就是AQS的releaseShared方法 sync.releaseShared(1); }
这里就是对Sync的releaseShared方法的一个封装,底层还是会进入的AQS的releaseShared方法,继续来看AQS的releaseShared模板方法:
public final boolean releaseShared(int arg) { // 1. 直接调用到子类的tryReleaseShared方法释放共享锁 if (tryReleaseShared(arg)) { // 2. 如果共享锁释放成功,将共享资源传播,唤醒等待队列的后续节点线程 doReleaseShared(); return true; } return false; }
这里又回到了之前讲解过的AQS的释放共享资源releaseShared的模板方法里面了:
(1)调用子类Sync的tryReleaseShared方法,去实际释放共享锁
(2)如果释放成功,则调用AQS的doReleaseShared方法唤醒等待队列中的线程,进行共享锁资源的传播,这里之前讲解AQS的时候讲解过了
我们画个图理解下:
核心的释放逻辑还是在类Sync的tryReleaseShared方法里面,我们继续分析。
4.1 Sync的tryReleaseShared源码实现
protected final boolean tryReleaseShared(int unused) { // 获取当前线程 Thread current = Thread.currentThread(); // 将当前线程加锁次数-1 if (firstReader == current) { // 如果之前只是加了一次锁,那么就释放锁了 if (firstReaderHoldCount == 1) firstReader = null; else // 如果加了多次锁,锁的次数减少1 firstReaderHoldCount--; } else { // 这里的逻辑,就是将ThreadLocal中自己存储的加锁次数减少1而已 // 没啥特殊的地方 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; } // 然后这里就是执行CAS减少加锁的次数,直到成功为止 for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; // cas修改读写锁变量state,将读锁次数-1 // 注意由于使用高16位表示读锁,所以单位值SHARED_UNIT if (compareAndSetState(c, nextc)) // 判断读锁的个数是否为0,如果为0说明读锁完全释放了 return nextc == 0; } }
我们画个图来理解一下:
这里就是读锁ReadLock释放锁的流程了,我们再来总结一下:
(1)释放锁首先判断一下,当前线程是否是第一个加锁线程,也就是current == firstReaderHoldCount (因为第一个加读锁的线程,加锁次数存储在firstReaderHoldCount中!!!,后续的加读锁的线程加锁次数存储在ThreadLocal中!!!)
(2)如果自己是第一个加锁线程,扣减firstReaderHolderCount次数,如果扣为零了,则将firstReaderHolderCount 置为null
(3)如果不是第一个加锁线程,从ThreadLocal中取出加锁次数,然后次数扣减1;如果加锁次数为零,从ThreadLocal中移除,因为不需要记录这个线程的加锁次数了,直接释放ThreadLocal的空间。否则还继续保存在ThreadLocal中
(4)执行CAS操作修改state读写锁的状态变量,注意这里由于是高16位表示读锁,所以读锁每减少1,则state 减少 65536
(5)不断重复CAS操作,直到成功为止,判断如果当前读锁个数为0,则读锁完全释放了返回1,否则返回-1
5 小结
到这里ReentrantReadWriteLock中读锁ReadLock加锁、释放锁的底层原理和源码我们就看的差不多了,有理解不对的地方欢迎指正哈。