前言
之前分析了使用AQS实现的共享锁和独占锁,今天来分析一下ReentrantReadWriteLock,这个即使用了共享锁(读锁)又使用了独享锁(写锁)的类。
与Mysql中的S锁(共享锁,读锁)一样,ReentrantReadWriteLock
中的读锁只允许继续加读锁,而不允许加写锁。
而写锁则与Mysql中X锁(排他锁)一样,不允许继续加任何锁,知道写锁被释放。
今天我们就来分析下ReentrantReadWriteLock
是如何做到的。
ReentrantReadWriteLock 源码分析
-
构造方法
ReentrantReadWriteLock 支持公平与非公平模式, 这点和ReentrantLock一样,构造函数中可以通过指定的值传递进去。ReentrantReadWriteLock 顾名思义,可重入的读写锁。
/**
* Creates a new {@code KReentrantReadWriteLock} with
* default (nonfair) ordering properties
* 用 nonfair 来构建 read/WriteLock (这里的 nonfair 指的是当进行获取 lock 时 若 aqs的syn queue 里面是否有 Node 节点而决定所采取的的策略)
*/
public ReentrantReadWriteLock(){
this(false);
}
/**
* 构建 ReentrantReadLock
*/
public ReentrantReadWriteLock(boolean fair){
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
和ReentrantLock一样,是否是公平锁是看获取锁的策略,而策略的实现要看内部类Sync的具体实现。
在ReentrantReadWriteLock
的构造方法中,除了创建一个内部类Sync对象,还创建了内部类ReadLock
对象以及内部类WriteLock
对象。
ReadLock构造方法
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
ReadLock构造方法将ReentrantReadWriteLock的sync属性赋给ReadLock的sync属性。
WriteLock构造方法
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
WriteLock构造方法将ReentrantReadWriteLock的sync属性赋给WriteLock的sync属性。
我们看到ReadLock与WriteLock都有sync属性,而且使用的是同一个Sync对象。
-
Sync中一些关键的属性
-
读写计数器
/** * ReentrantReadWriteLock 使用 AQS里面的 state的高低16位来记录 read /write 获取的次数(PS: writeLock 是排他的 exclusive, readLock 是共享的 shared ) * 记录的操作都是通过 CAS 操作(有竞争发生) * * 特点: * 1) 同一个线程可以拥有 writeLock 与 readLock (但必须先获取 writeLock 再获取 readLock, 反过来进行获取会导致死锁) * 2) writeLock 与 readLock 是互斥的(就像 Mysql 的 X S 锁) * 3) 在因 先获取 readLock 然后再进行获取 writeLock 而导致 死锁时, 本线程一直卡住在对应获取 writeLock 的代码上(因为 readLock 与 writeLock 是互斥的, 在获取 writeLock 时监测到现在有线程获取 readLock , 锁一会一直在 aqs 的 sync queue 里面进行等待), 而此时 * 其他的线程想获取 writeLock 也会一直 block, 而若获取 readLock 若这个线程以前获取过 readLock, 则还能继续 重入 (reentrant), 而没有获取 readLock 的线程因为 aqs syn queue 里面有获取 writeLock 的 Node 节点存在会存放在 aqs syn queue 队列里面 一直 block */ /** 对 32 位的 int 进行分割 (对半 16) */ static final int SHARED_SHIFT = 16; static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 000000000 00000001 00000000 00000000 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 000000000 00000000 11111111 11111111 static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 000000000 00000000 11111111 11111111 /** Returns the number of shared holds represented in count */ /** 计算 readLock 的获取次数(包含重入的次数) */ static int sharedCount(int c) { return c >>> SHARED_SHIFT; } // 将字节向右移动 16位, 只剩下 原来的 高 16 位 /** Returns the number of exclusive holds represented in count */ /** 计算 writeLock 的获取的次数(包括重入的次数) */ static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } // 和EXCLUSIVE_MASK 与 也就是只取低16位
之前我们说过了读写锁使用的是同一个Sync对象,之前我们分析的独占锁或者共享锁,都是通过判断state状态来完成获取锁或者释放锁的逻辑的,那么这里有独占锁和共享锁共同存在,依然还是需要state这个一个字段来判断获取锁或释放锁的逻辑(我们依旧需要使用一个字段来判断,因为读锁和写锁不是完全孤立的,写锁会阻塞写锁和读锁,读锁会阻塞写锁,所以用两个字段来判断,反而会增加处理复杂度),需要怎样做呢?Doug Lea大师给出的答案是,state的高16位来记录 read 获取的次数,低16位来记录write获取的次数,具体的实现非常精彩,通过位操作,巧妙的使用state字段来标识了读锁和写锁的关系。
-
线程获取读锁次数统计相关属性
/** - A counter for per-thread read hold counts - Maintained as a ThreadLocal; cached in cachedHoldCounter */ /** - 几乎每个获取 readLock 的线程都会含有一个 HoldCounter 用来记录 线程 id 与 获取 readLock 的次数 ( writeLock 的获取是由 state 的低16位 及 AQS中的exclusiveOwnerThread 来进行记录) - 这里有个注意点 第一次获取 readLock 的线程使用 firstReader, firstReaderHoldCount 来进行记录 - (PS: 不对, 我们想一下为什么不 统一用 HoldCounter 来进行记录呢? 原因: 所有的 HoldCounter 都是放在 ThreadLocal 里面, 而很多有些场景中只有一个线程获取 readLock 与 writeLock , 这种情况还用 ThreadLocal 的话那就有点浪费(ThreadLocal.get() 比直接 通过 reference 来获取数据相对来说耗性能)) */ static final class HoldCounter { int count = 0; // 重复获取 readLock/writeLock 的次数 // Use id, not reference, to avoid garbage retention final long tid = getThreadId(Thread.currentThread()); // 线程 id } /** - ThreadLocal subclass, Easiest to explicitly define for sake - of deserialization mechanics */ /** 简单的自定义的 ThreadLocal 来用进行记录 readLock 获取的次数 */ static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter>{ @Override protected HoldCounter initialValue() { return new HoldCounter(); } } /** - The number of reentrant read locks held by current thread. - Initialized only in constructor and readObject - Removed whenever a thread's read hold count drops to 0 */ /** - readLock 获取记录容器 ThreadLocal(ThreadLocal 的使用过程中当 HoldCounter.count == 0 时要进行 remove , 不然很有可能导致 内存的泄露) */ private transient ThreadLocalHoldCounter readHolds; /** - 最后一次获取 readLock 的 HoldCounter 的缓存 - (PS: 还是上面的问题 有了 readHolds 为什么还需要 cachedHoldCounter呢? 在非常多的场景中, 这次进行release readLock的线程就是上次 acquire 的线程, 这样直接通过cachedHoldCounter来进行获取, 节省了通过 readHolds 的 lookup 的过程) */ private transient HoldCounter cachedHoldCounter; /** - 下面两个是用来进行记录 第一次获取 readLock 的线程的信息 - 准确的说是第一次获取 readLock 并且 没有 release 的线程, 一旦线程进行 release readLock, 则 firstReader会被置位 null */ private transient Thread firstReader = null; private transient int firstReaderHoldCount;
- ThreadLocalHoldCounter继承了ThreadLocal,一个存储HoldCounter类型对象的ThreadLocal实例,并且重写了
initialValue()
方法,这意味着,当调用get()方法时,如果之前没有设置值,将会调用initialValue()
生成value(key为当前ThreadLocal实例,一个ThreadLocalHoldCounter对象)放入到当前线程的ThreadLocalMap中。 - HoldCounter 用来记录当前线程获取读锁次数的一个类
- ThreadLocalHoldCounter继承了ThreadLocal,一个存储HoldCounter类型对象的ThreadLocal实例,并且重写了
-
-
ReadLock 读锁的实现
-
ReadLock#lock
public void lock() { sync.acquireShared(1); }
同样是直接调用AQS的final方法
acquireShared()
,直接看定义何时成功获到共享锁的方法的具体实现。以非公平锁为例。Sync#tryAcquireShared()
protected final int tryAcquireShared(int unused) { /* * Walkthrough: * 1. If write lock held by another thread, fail. * 2. Otherwise, this thread is eligible for * lock wrt state, so ask if it should block * because of queue policy. If not, try * to grant by CASing state and updating count. * Note that step does not check for reentrant * acquires, which is postponed to full version * to avoid having to check hold count in * the more typical non-reentrant case. * 3. If step 2 fails either because thread * apparently not eligible or CAS fails or count * saturated, chain to version with full retry loop. */ Thread current = Thread.currentThread(); int c = getState(); //state低16位标识写锁,查看是否存在写锁 if (exclusiveCount(c) != 0 && //查看当前占有AQS的线程是否是当前线程 getExclusiveOwnerThread() != current) //如果条件都不满足,则返回-1,当前线程进入自旋尝试获取共享锁,自旋获取是则被挂起 return -1; //取高16位读锁,查看拥有读锁个数 int r = sharedCount(c); //readerShouldBlock()是Sync这个抽象类中的抽象方法。在NonFairSync和FairSync中有不同的实现 ,如果这个方法返回true的话就会导致fullTryAcquireShared的执行 if (!readerShouldBlock() && r < MAX_COUNT && //读锁加1,我们看到读锁加1会将state+2^16,这就导致了state的高16位代表读锁个数 compareAndSetState(c, c + SHARED_UNIT)) { //进入这块逻辑,会返回1,不会去尝试自旋获取共享欧锁,而是结束方法,线程继续往下执行 if (r == 0) { firstReader = current; firstReaderHoldCount = 1; } //如果当前线程是重入的,第一次读的线程就是当前线程 else if (firstReader == current) { firstReaderHoldCount++; } else { // 非 firstReader 读锁重入计数更新 HoldCounter rh = cachedHoldCounter; //cachedHoldCounter为null或者当前线程id不是cachedHoldCounter的线程id if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1; } //第一次获取读锁失败,有两种情况: //1)没有写锁被占用时,尝试通过一次CAS去获取锁时,更新失败(说明有其他读锁在申请) //2)当前线程占有写锁,并且有其他写锁在当前线程的下一个节点等待获取写锁,除非当前线程的下一个节点被取消,否则fullTryAcquireShared也获取不到读锁\ //代码调用 fullTryAcquireShared 大体情况是 AQS 的 sync queue 里面有其他的节点 或 AQS queue 的 head.next 是个获取 writeLock 的节点, 或 CAS 操作 state 失败 return fullTryAcquireShared(current); }
1.如果存在写锁,并且持有写锁的线程不是当前线程,则返回-1,当前线程进入自旋获取锁的过程。
2.如果当前获取读锁的操作不需要被阻塞并且CAS增加读锁计数成功且没有达到读锁个数限制,进入记录当前线程 获取 readLock 的次数的逻辑,最后返回1,成功获取到读锁。
3.如果前两种情况都不符合,那么就执行
fullTryAcquireShared
方法。
2中当前获取读锁的操作是否需要被阻塞时,公平锁和非公平锁的实现不同
NonFairSync#readerShouldBlock()
final boolean readerShouldBlock() { /* As a heuristic to avoid indefinite writer starvation, * block if the thread that momentarily appears to be head * of queue, if one exists, is a waiting writer. This is * only a probabilistic effect since a new reader will not * block if there is a waiting writer behind other enabled * readers that have not yet drained from the queue. */ return apparentlyFirstQueuedIsExclusive(); } //AQS final boolean apparentlyFirstQueuedIsExclusive() { Node h, s; return (h = head) != null && (s = h.next) != null && !s.isShared() && s.thread != null; }
我们看到NonFairSync类中对是否需要阻塞读锁的实现实际上调用的是AQS中的方法,其实就是查看第一个线程Node节点是独占类型节点(即是写锁Node节点),如果是的话,就需要阻塞读。
FairSync#readerShouldBlock()
final boolean readerShouldBlock() { return hasQueuedPredecessors(); } public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; //如果队列中存在线程Node节点, return h != t && //第一个线程Node节点不是当前节点 ((s = h.next) == null || s.thread != Thread.currentThread()); }
如果队列中存在线程Node节点且点一个线程Node节点不是当前线程,那么就要阻塞当前读。
2中记录当前线程 获取 readLock 的次数的逻辑分析
if (r == 0) { firstReader = current; firstReaderHoldCount = 1; } //如果当前线程是重入的,第一次读的线程就是当前线程 else if (firstReader == current) { firstReaderHoldCount++; } else { // 非 firstReader 读锁重入计数更新 HoldCounter rh = cachedHoldCounter; //cachedHoldCounter为null或者当前线程id不是cachedHoldCounter的线程id if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1;
1.如果当前读锁计数为0,那么将当前线程置为firstReader线程,firstReaderHoldCount置为1.
2.如果当前线程是重入的,第一次读的线程就是当前线程,之前将 firstReaderHoldCount++;
3.否则,找到之前缓存的cachedHoldCounter。
3.1如果缓存的HoldCounter为空或者缓存HoldCounter不是属于当前线程的HoldCounter。那么就将 cachedHoldCounter置为当前线程的HoldCounter。
3.2 如果缓存的HoldCounter属于当前线程,如果当前线程的读锁计算为0,那么就将cachedHoldCounter设置为当前线程的HoldCounter。这个时候为0的情况,只可能是释放共享锁的方法调用了
readHolds.remove();
将当前线程的ThreadLocalMap中以readHolds为key的Entry删除掉,所以这里需要重置。3.3 3.1或3.2执行完后均将HoldCounter的count+1。
fullTryAcquireShared
/** * Full version of acquire for reads, that handles CAS misses * and reentrant reads not dealt with in tryAcquireShared. */ /** * fullTryAcquireShared 这个方法其实是 tryAcquireShared 的冗余(redundant)方法, 主要补足 readerShouldBlock 导致的获取等待 和 CAS 修改 AQS 中 state 值失败进行的修补工作 */ final int fullTryAcquireShared(Thread current){ /** * This code is part redundant with that in * tryAcquireShared but is simpler overall by not * complicating tryAcquireShared with interactions between * retries and lazily reading hold counts */ HoldCounter rh = null; for(;;){ int c= getState(); if(exclusiveCount(c) != 0){ if(getExclusiveOwnerThread() != current) // 1. 若此刻 有其他的线程获取了 writeLock 则当前线程要进入自旋获取读锁的过程,宿命大概就是被挂起了 return -1; // else we hold the exclusive lock; blocking here // would cause deadlock }else if(readerShouldBlock()){ // 2. 判断 获取 readLock 的策略 // Make sure we're not acquiring read lock reentrantly if(firstReader == current){ // 3. 若是 readLock 的 重入获取, 则直接进行下面的 CAS 操作 // assert firstReaderHoldCount > 0 }else{ if(rh == null){ rh = cachedHoldCounter; if(rh == null || rh.tid != getThreadId(current)){ rh = readHolds.get(); if(rh.count == 0){ readHolds.remove(); // 4. 若 rh.count == 0 进行 ThreadLocal.remove } } } if(rh.count == 0){ // 5. count != 0 则说明这次是 readLock 获取锁的 重入(reentrant), 所以即使出现死锁, 以前获取过 readLock 的线程还是能继续 获取 readLock return -1; // 6. 进行到这一步只有 当 aqs sync queue 里面有 获取 readLock 的node 或 head.next 是获取 writeLock 的节点 } } } if(sharedCount(c) == MAX_COUNT){ // 7. 是否获取 锁溢出 throw new Error("Maximum lock count exceeded"); } if(compareAndSetState(c, c + SHARED_UNIT)){ // 8. CAS 可能会失败, 但没事, 我们这边外围有个 for loop 来进行保证 操作一定进行 if(sharedCount(c) == 0){ // 9. r == 0 没有线程获取 readLock 直接对 firstReader firstReaderHoldCount 进行初始化 firstReader = current; firstReaderHoldCount = 1; }else if(firstReader == current){ // 10. 第一个获取 readLock 的是 current 线程, 直接计数器加 1 firstReaderHoldCount++; }else{ if(rh == null){ rh = cachedHoldCounter; } if(rh == null || rh.tid != getThreadId(current)){ rh = readHolds.get(); // 11. 还是上面的逻辑, 先从 cachedHoldCounter, 数据不对的话, 再从readHolds拿数据 }else if(rh.count == 0){ readHolds.set(rh); // 12. 为什么要 count == 0 时进行 ThreadLocal.set? 因为上面 tryReleaseShared方法 中当 count == 0 时, 进行了ThreadLocal.remove } rh.count++; cachedHoldCounter = rh; // cache for release // 13. 获取成功 } return 1; } } }
1.如果写锁存在,并且占有写锁的线程不是当前线程,那么直接返回-1,当前线程将进入自旋获取读锁,宿命大概就是被挂起。
2.否则进行是否需要阻塞读的判断
2.1 如果需要阻塞读
2.1.1如果首次获取读锁的线程是当前线程,不做处理
2.1.2 否则
2.1.2.1 如果rh这个局部变量为null,那么将缓存HoldCounter指向该变量。如果缓存HoldCounter所属的线程不是当前线程,那么吧rh指向当前线程的HoldCounter,这时,如果rh.count依旧为0,则remove掉当前线程的HoldCounter。
2.1.2.2 如果rh这个局部变量不为null,如果rh.count为0,那么返回-1.
3.如果之前的操作都没有导致return退出循环,那么先判断读锁计数器是否溢出,溢出抛出异常。
4.CAS设置新的读锁计数器,通过不断的循环来保证成功。然后开始增加当前线程 获取 readLock 的次数,最后返回1,结束获取读锁的方法。
-
ReadLock#unlock
public void unlock() { sync.releaseShared(1); }
也是直接使用了AQS的final方法
releaseShared()
,我们直接看tryReleaseShared()
方法。
protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); //1.如果当前线程就是首次获取读锁的线程 if (firstReader == current) { // assert firstReaderHoldCount > 0; //1.1如果当前线程的读重入次数为1,将首次获取读锁的线程置为空 if (firstReaderHoldCount == 1) firstReader = null; //1.2否则只是将读重入次数减少一次 else firstReaderHoldCount--; } //2.如果不是 else { //获取上一次获取读锁的线程的HoldCounter HoldCounter rh = cachedHoldCounter; //2.1如果缓存的HoldCounter为空或者当前线程不是之前缓存的HoldCounter所属的线程 if (rh == null || rh.tid != getThreadId(current)) //获取当前线程的HoldCounter 如果之前没有,侧会实例化一个新的 rh = readHolds.get(); int count = rh.count; //2.2当重入次数<=1的时候,我们需要清除掉当前线程的HoldCounter if (count <= 1) { //这里会remove();防止内存泄漏 readHolds.remove(); if (count <= 0) throw unmatchedUnlockException(); } //重入次数-1 --rh.count; } //3.读锁减一 for (;;) { //获取当前的state int c = getState(); //将读锁计数器计数减一 int nextc = c - SHARED_UNIT; //CAS设置新的state值,直到成功才退出循环 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. //判断是否读锁为0,如果为0返回true,否则就返回false return nextc == 0; } }
1.对线程的读锁重入次数进行减一操作
-
1.1如果当前线程就是首次获取读锁的线程,那么对firstReader和firstReaderHoldCount进行操作。
1.2如果不是,那么对cachedHoldCounter进行操作,如果当前线程与之前缓存HoldCounter所属线程不一
致,需要获取到属性当前线程的HoldCounter(如果当前线程没有HoldCounter,需要实例化一个),然后
进行读锁重入次数减一的操作。
2.使用循环CAS完成对读锁计数减一,当读锁计数为0的时候,唤醒阻塞的线程。
-
WriteLock写锁的实现
-
WriteLock.lock()
public void lock() { sync.acquire(1); }
依旧是调用AQS的final方法
acquire()
方法,我们直接看tryAcquire()
方法的实现。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) { // (Note: if c != 0 and w == 0 then shared count != 0) //如果state不为0的同时写锁计数为0,意味着当前有读锁,需要返回false //或者当前线程并不是占有写锁的线程返回false if (w == 0 || current != getExclusiveOwnerThread()) return false; //如果写锁计数大于最大限制,抛出异常 if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // Reentrant acquire //写锁计数加一 ,不用CAS,因为写锁是独占锁 setState(c + acquires); return true; } if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; //如果CAS设置成功,将当前线程设置为占有AQS的线程,返回true 为什么不需要CAS设置呢? setExclusiveOwnerThread(current); return true; }
1.如果state不为0,那么就意味着当前有读锁或者写锁
1.1如果写锁计算为0,那么就意味着现在有读锁,那么返回false,进入自旋获取写锁。如果当前线程不是占有写锁的线程,那么同样返回false.
1.2判断是否写锁已经饱和了,饱和了抛出异常。
1.3 如果前两步都没有导致方法提前返回,那么就开始讲写锁计数加一,并且返回true,这里不需要使用CAS设置,因为写锁是独占锁。
2.如果state为0,那么就意味着当前没有读写锁,但是此时可能有多个尝试获取写锁的线程执行到这一步,而且这时可能已经有尝试获取读锁的线程将state改动了,不再为0了,并发情况下,只要不是原子操作都可能出现问题,所以这个时候我们需要使用CAS来设置,这样就保证了并发情况下,只有一个线程会被设置为占有写锁的线程。
-
WriteLock.unlock()
public void unlock() { sync.release(1); } ```
调用AQS的final方法
release()
方法,照例直接看tryRelease()
方法。
protected final boolean tryRelease(int releases) { //1.是否是当前线程持有写锁,不是的话,抛出异常 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); int nextc = getState() - releases; //查看写锁计数是否为0,如果为0就会返回true,接着就会在CLH中存在阻塞线程的时候去唤醒阻塞节点。 boolean free = exclusiveCount(nextc) == 0; if (free) setExclusiveOwnerThread(null); setState(nextc); return free; }
-
总结
本文只是对读写锁进行一个简单的分析,这个类很有更多更深入的内容,有兴趣的可以阅读源码进行深入分析。