AQS系列(四)- ReentrantReadWriteLock读写锁的释放锁
前言
继续JUC包中ReentrantReadWriteLock的学习,今天学习释放锁。
一、写锁释放锁
入口方法
1 public void unlock() { 2 sync.release(1); 3 }
进入AQS追踪release方法:
1 public final boolean release(int arg) { 2 if (tryRelease(arg)) { 3 Node h = head; 4 if (h != null && h.waitStatus != 0) 5 unparkSuccessor(h); 6 return true; 7 } 8 return false; 9 }
可见跟ReentrantLock调用的同一个释放锁方法,不同点就是tryRelease方法,所以此处只看此方法即可。读写锁tryRelease方法的实现在其内部类Sync中封装,如下所示:
1 protected final boolean tryRelease(int releases) { 2 if (!isHeldExclusively()) // 判断当前线程是不是记录的独占线程,不是的话不能释放 3 throw new IllegalMonitorStateException(); 4 int nextc = getState() - releases; 5 boolean free = exclusiveCount(nextc) == 0; // 判断减完之后是不是0,是0的话说明当前线程都释放了,将独占线程设置为空,后面排队的可以抢占锁了 6 if (free) 7 setExclusiveOwnerThread(null); 8 setState(nextc); 9 return free; 10 }
跟ReentrantLock中唯一不同的地方是对于free的赋值,因为写锁的重入次数是记录在state的低16位上,所以此处要获取一下,其余的逻辑都一样。
二、读锁释放锁
1 public void unlock() { 2 sync.releaseShared(1); 3 }
进入AQS追踪releaseShared方法:
1 public final boolean releaseShared(int arg) { 2 if (tryReleaseShared(arg)) { 3 doReleaseShared(); 4 return true; 5 } 6 return false; 7 }
只有两个关键方法tryReleaseShared和doReleaseShared,下面分别看看它们的实现逻辑。
1、tryReleaseShared
1 protected final boolean tryReleaseShared(int unused) { 2 Thread current = Thread.currentThread(); 3 if (firstReader == current) {// 如果当前线程是第一个获取锁的,因为有两个成员变量直接记录,所以只要修改这两个成员变量的值即可 4 // assert firstReaderHoldCount > 0; 5 if (firstReaderHoldCount == 1) // count为1,此时不应该把它置为0吗 6 firstReader = null; 7 else 8 firstReaderHoldCount--; 9 } else { // 不是当前线程,则要去缓存获取或者本地线程变量中获取当前线程的重入次数,给它减一,如果次数小于等于1则直接移除 10 HoldCounter rh = cachedHoldCounter; 11 if (rh == null || rh.tid != getThreadId(current)) 12 rh = readHolds.get(); 13 int count = rh.count; 14 if (count <= 1) { 15 readHolds.remove(); 16 if (count <= 0) 17 throw unmatchedUnlockException(); 18 } 19 --rh.count; 20 } // 维护state的锁重入次数/获取次数记录 21 for (;;) { 22 int c = getState(); 23 // 因为是读锁,所以一个SHARED_UNIT的值代表一个锁 24 int nextc = c - SHARED_UNIT; 25 if (compareAndSetState(c, nextc)) 26 // 如果只有读锁,这里返回什么都无所谓;所以此返回值是专门为写锁准备的,后续会根据返回值去唤醒写锁, 27 return nextc == 0; 28 } 29 }
该方法逻辑很清晰,for循环上面的部分代码,用户将读锁当前线程记录的重入次数-1;for循环用于将AQS中维护的state中的读锁占有次数-1.返回的布尔类型用于给后续方法判断是否要唤醒写锁。后续方法即我们下一步要追踪的doReleaseShared方法。
2、doReleaseShared
1 private void doReleaseShared() { 2 // 唤醒后续的写线程 3 for (;;) { 4 Node h = head; 5 if (h != null && h != tail) { 6 int ws = h.waitStatus; 7 if (ws == Node.SIGNAL) { // 第一种:如果状态是-1,说明后面肯定有阻塞的任务,要去唤醒它 8 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) 9 continue; // loop to recheck cases 10 unparkSuccessor(h); 11 } // ws等于0说明是最后一个节点了,此时将Node的ws设置为PROPAGATE,因为后续没有节点了,所以不用唤醒 12 else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) 13 continue; // loop on failed CAS 14 } 15 if (h == head) // 不等于head说明又有新的读锁进来了,这时要继续循环 16 break; 17 } 18 }
此方法用于唤醒读锁后处于挂起状态的锁,读锁后处于挂起状态的锁有两种:第一种是写锁,这很好理解,如果有读锁被占用,写锁过来的时候肯定需要挂起等读锁执行完(非相同线程),读锁执行完之后唤醒这个写锁;第二种是读锁,为什么当前执行的是读锁而后面还会有读锁被挂起呢?这就要回到系列(三)中的内容了,在系列(三)读锁加锁中我们讲过一个 apparentlyFirstQueuedIsExclusive 方法,该方法会判断队列中第一个排队的是不是写锁,如果是写锁则让当前的读锁挂起不去竞争锁,而若在队首写锁等待的过程中有多个读锁过来,则这多个读锁都会被依次挂起,这时就会出现第二种情况,即读锁执行的时候后面还有一个读锁被挂起,执行完之后需唤醒它。
此处第二种读锁唤醒读锁的场景,是在读锁加锁时触发的。在系列(三)中对这里未涉及,现在我们再回头看看。在doAcquireShared方法中,有个setHeadAndPropagate方法,在该方法中会检测下一个节点是不是读锁,如果是就调用doReleaseShared方法唤醒它。
小结
读写锁的释放锁逻辑基本就这些了,下面再做一个小结。
写锁释放逻辑跟ReentrantLock中的释放锁逻辑基本一致,因为毕竟都是独占锁。
读锁释放则复杂的多,它会先释放每个读锁线程记录的重入次数,再去减掉state中记录的加锁次数,最后还要唤醒后面挂起的线程。唤醒挂起的线程又分两种情况,一种是唤醒后面的写锁线程,另一种是唤醒读锁线程。读锁之间不互斥为什么在读锁执行时还会有读锁被挂起?是因为在读锁加锁时为防止写锁饥饿如果判断队首有写锁在等待获取锁那么后来的读锁都要挂起等待,这时就会出现多个读锁被挂起的情况。在释放读锁时唤醒的线程是写锁线程,在读锁加锁时唤醒的线程是读锁线程。
另外,对于Node.PROPAGATE这个状态一直没看出它的作用,而且查看了一下使用的地方,发现只在上面的doReleaseShared方法中用过,所以个人觉得是个可有可无的状态,不知道是为后续扩展留的状态还是有其他作用我没看出来,如果有对此有理解的园友,欢迎给答疑解惑一下,感谢!