ReentrantReadWriteLock 源码分析以及 AQS 共享锁 (二)

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

ReentrantReadWriteLock 源码分析以及 AQS 共享锁 (二)

烟雨星空   2020-03-17 我要评论
## 前言 上一篇讲解了 AQS 的独占锁部分(参看:[ReentrantLock 源码分析以及 AQS (一)](https://mp.weixin.qq.com/shttps://img.qb5200.com/download-x/dDjbR76U5C696CXAOAvpng)),这一篇将介绍 AQS 的共享锁,以及基于共享锁实现读写锁分离的 ReentrantReadWriteLock。(若是遇到之前讲过的方法,将不再赘述) **先思考一下,为什么我们用读写锁分离?** 我们知道 ReentrantLock 用的是独占锁,不管线程是读还是写状态,都会阻塞,这无疑会降低并发量。 但是,我们知道多个线程同时去读数据的时候,并不会产生线程安全的问题,因为它们互不干扰。那么为什么不设计一种方案,让所有的读线程可以共享,一起同时读数据呢,只需要阻塞写的线程就可以了。提高并发的同时,也不会产生数据不一致的现象。 同样的,如果有线程在写数据,那么也会阻塞其它读线程(同样阻塞其它写线程),数据写完之后才可以读数据,这样保证读到的数据都是最新的。 因此,我们可以用读、写两把锁,分别控制数据的读和写。实现读读共享、读写互斥,写写互斥。这也是 ReentrantReadWriteLock 读写分离锁的由来。它非常适合用在读多写少的场景。 ## ReentrantReadWriteLock 它和 ReentrantLock 一样,也是一个可重入的锁,并基于 AQS 共享锁实现了读写分离。其内部结构也大同小异,支持公平锁和非公平锁。我们看下它的构造函数, ``` public ReentrantReadWriteLock() { //默认非公平 this(false); } public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } ``` 它定义了两个内部类来表示读锁和写锁,并且都通过内部类 Sync 来实现加锁,释放锁等功能。 ``` public static class ReadLock implements Lock, java.io.Serializable { private static final long serialVersionUID = -5992448646407690164L; private final Sync sync; protected ReadLock(ReentrantReadWriteLock lock) { sync = lock.sync; } ... } public static class WriteLock implements Lock, java.io.Serializable { private static final long serialVersionUID = -4992448646407690164L; private final Sync sync; protected WriteLock(ReentrantReadWriteLock lock) { sync = lock.sync; } ... } abstract static class Sync extends AbstractQueuedSynchronizer { } ``` 我们再看下公平锁和非公平锁,其中有两个比较重要的方法,用来判断读锁和写锁是否应该被阻塞,后面加锁的时候会用到(其实,实际情况是否真的应该阻塞,还需要斟酌,后面会说)。 ``` static final class FairSync extends Sync { private static final long serialVersionUID = -2274990926593161451L; //公平锁的读和写都需要判断,在它前面是否已经有线程在等待。 //有的话,当前线程就需要阻塞,这也体现了公平性。 final boolean writerShouldBlock() { return hasQueuedPredecessors(); } final boolean readerShouldBlock() { return hasQueuedPredecessors(); } } static final class NonfairSync extends Sync { private static final long serialVersionUID = -8159625535654395037L; //非公平锁,写的时候不需要阻塞,直接返回false final boolean writerShouldBlock() { return false; // writers can always barge } final boolean readerShouldBlock() { //为了避免写线程饥饿,需要判断同步队列中第一个排队的(head.next)是否是独占锁(写线程) //如果是的话,当前读线程就需要阻塞,这是 AQS 中的方法 return apparentlyFirstQueuedIsExclusive(); } } final boolean apparentlyFirstQueuedIsExclusive() { Node h, s; return (h = head) != null && (s = h.next) != null && !s.isShared() && s.thread != null; } ``` **思考:** 我们知道 ReentrantLock 的同步状态和重入次数,是直接用 state 值来表示的。那么,现在我需要读和写两把锁,怎么才能用一个 int 类型的值来表示两把锁的状态呢?并且,锁是可重入的,重入的次数怎么记录呢? 别急,下面一个一个说。 ### 怎么用一个 state 值表示读、写两把锁? ![](https://img2020.cnblogs.com/other/1714084/202003/1714084-20200317204510064-1840955138.jpg) state 是一个 32 位的 int 值,读写锁中,把它一分为二,高 16 位用来表示读状态,其值代表读锁的线程数,如图中为 3 个,低 16位表示写状态,其值代表写锁的重入次数(因为是独占锁)。 这样,就可以分别计算读锁和写锁的个数了。其相关的属性和方法定义在 Sync 类中。 ``` static final int SHARED_SHIFT = 16; //表明读锁每增加一个,state的实际值增加 2^16 static final int SHARED_UNIT = (1 << SHARED_SHIFT); //写锁的最大重入次数,读锁的最大个数 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; //持有读锁的线程个数,参数如的 c 代表 state值 //state 的32位二进制位,无符号右移 16位之后,其实就是高16位的值 static int sharedCount(int c) { return c >>> SHARED_SHIFT; } //写锁数量,即写锁的重入次数 static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } ``` 读锁的个数计算比较简单,直接无符号右移 16 位即可。我们看下写锁的重入次数是怎么计算的。先看下 EXCLUSIVE_MASK 这个值,是 (1 << 16) - 1,我们用二进制表示计算过程为: ``` // 1的二进制 0000 0000 0000 0000 0000 0000 0000 0001 // 1左移 16位 0000 0000 0000 0001 0000 0000 0000 0000 //再减 1 0000 0000 0000 0000 1111 1111 1111 1111 //任何一个 32位二进制数 c,和以上值做 “与” 运算都为它本身 c 的低 16 位值 //这个不用解释了吧,这个不会的话,需要好好补充一下基础知识了。。。 ``` ### 锁的重入次数是怎么计算的? 写锁比较简单,直接用计算出来的低16位值就可以代表写锁的重入次数。 读锁,就比较复杂了,因为高16位只能表示持有共享锁的线程个数,实在是分身乏术啊。所以,在 Sync 内部,维护了一个类,用来表示每个线程重入的次数, ``` static final class HoldCounter { int count = 0; // Use id, not reference, to avoid garbage retention final long tid = getThreadId(Thread.currentThread()); } ``` 这里边定义了一个计数器来表示重入次数,tid 来表示当前的线程 id 。但是,这样还不够,我们需要把 HoldCounter 和 线程绑定,这样才可以区分出来每个线程分别持有的锁个数(重入次数),这就需要用到 ThreadLocal 了。 ``` static final class ThreadLocalHoldCounter extends ThreadLocal

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们