读写锁ReentrantReadWriteLock
接口源码注释
是接口ReadWriteLock的实现。此接口中维护了2个方法一个返回读锁,一个返回写锁。读锁是共享锁,可以同时被多个线程持有。写锁是独占锁,只能同时被一个线程持有。所有读写锁接口的实现必须都要保证在获取读锁时,读锁线程能看到上一个释放的写锁的线程所做的更改。保持可见性。读写锁与一般的并发锁比起来有更高的并发性(实际上,只有在多处理器上,并且只有在共享数据的访问模式合适的情况下,才能完全实现并发性的增加.实际上,读写锁能否提高性能取决于与被修改的数据相比,读取和写入操作的持续时间,以及对数据的争用,即同时尝试读写数据的线程数。假如一个数据在大多数情况下只是被读(例如某种目录)是使用读写锁的首选。但是,如果更新变得频繁,那么数据的大部分时间都被独占锁定,并且并发性几乎没有增加。此外,如果读操作太短,读写锁实现的开销(本质上比互斥锁更复杂)可以控制执行成本,特别是许多读写锁实现仍然通过一小段代码序列化所有线程最终,只有分析和测量才能确定读写锁的使用是否适合您的应用程序。)。虽然每次同时只有一个线程能修改共享数据,但在许多情况下,任意数量的线程可以并发的读取数据。
虽然读写锁的基本操作是直接的,但是实现必须做出许多策略决策,这可能会影响给定应用程序中读写锁的有效性。这些策略决策包括:
当读线程和写线程都在等待时,在写线程释放写锁时,确定是授予读锁还是写锁
写线程首选是常见的,因为写操作被认为是短而不频繁的
读线程优先不太常见,因为如果读线程是频繁的并且如预期的那样寿命长,那么它会导致写操作的长时间延迟
公平或“有序”的实现也是可能的
确定在读线程处于活动状态且写线程正在等待时请求读锁的读线程是否被授予读锁
对读线程的偏好可能会无限期地延迟写线程,而对写线程的偏好则会降低并发的可能性
源码注释总结
一个ReadWriteLock锁包含一对关联的锁:读锁和写锁
读锁readLock是共享的。
写锁writeLock是独占的。
理论上,读写锁比互斥锁有更好的性能体现的
读写锁更适用于读多写少的情景
实际上,读写锁是否能够带来性能的提升,是需要实际的测试与配置的。
ReentrantReadWriteLock注释
ReentrantReadWriteLock类是ReadWriteLock接口的实现,支持与之类似的语法。该类具有以下属性:
获取顺序:这个类不会强行指定访问锁的读写顺序,但是它支持一个可选的公平策略
非公平模式(默认):在非公平模式下,进入读锁和写锁的顺序取决于可重入规则,是不确定的;连续争用可能会导致一个或者多个读线程或写线程进入无限等待状态。非公平锁会比公平锁有更高的吞吐量
公平模式:在公平模式下,进入读锁和写锁的顺序使用一种近乎顺序的策略。当当前持有的锁被释放时,等待时间最长的单个写线程会被授予写锁,或者如果有一组读线程比所有的写线程等待的时间都长,则这组读线程将被授予锁。如果有写线程持有锁或者有写线程正在等待锁,试图去获取一个公平的读锁(不可重入)的读线程将被阻塞。这个读线程不会获得锁,直到当前等待的所有写线程获取并释放了锁。当然,如果写线程放弃了等待,使得等待队列中只剩下一个或者多个等待时间最长的读线程,并且当前读锁可用,则这些读线程将会被授予锁。
除非当前的读锁和写锁都是可用的,写线程尝试去获取一个公平的写锁(不可重入)才不会被阻塞。
可重入性:ReentrantReadWriteLock类定义的锁,允许读线程和写线程以 ReentrantLock的形式去获取读锁和写锁。直到所有持有锁的写线程释放锁,不可重入的读线程才会被允许获取锁。此外,写线程可以获取读锁,反过来,读线程不可以获取写锁。
可降级性:可重入性也允许写锁降级成为读锁:首先获取写锁,然后获取读锁,然后释放写锁。
但是,从读锁升级为写锁是不可能的。
可中断性:在尝试获取锁的过程中,读锁和写锁都可以被中断。
支持Condition:写锁支持,但是读锁不支持
监视状态:ReentrantReadWriteLock类提供了方法用于查看锁是被持有还是被争用。这些方法是为监视系统状态而设计的,而不是用于同步控制
读锁与写锁之间的并存关系
写写互斥(不包括锁的重入)
写读互斥(不包括同一线程先获取写锁后获取读锁)
读写互斥
读读共享
ReentrantReadWriteLock源码解析
基础知识铺垫
(原码、反码、补码是机器存储一个具体数字的编码方式):
机器数:一个数在计算机中的二进制表示形式,叫做这个数的机器数。机器数是带符号的,在计算机用机器数的最高位存放符号,正数为0,负数为1。
真值:因为第一位是符号位,所以机器数的形式值就不等于真正的数值。
假如有符号数 1000 0011,其最高位1代表负,其真正数值是 -3,而不是形式值131(1000 0011转换成十进制等于131)。所以,为区别起见,将带符号位的机器数对应的真正数值称为机器数的真值。
例:0000 0001的真值 = +000 0001 = +1,1000 0001的真值 = –000 0001 = –1
原码:原码就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。比如:如果是8位二进制:
[+1]原= 0000 0001 [-1]原= 1000 0001 。第一位是符号位,因为第一位是符号位,所以8位二进制数的取值范围就是:[1111 1111 , 0111 1111]。即[-127 , 127](即第一位不表示值,只表示正负。)
反码:正数的反码是其本身。负数的反码是在其原码的基础上,符号位不变,其余各个位取反。
补码:正数的补码就是其本身。负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后+1。(也即在反码的基础上+1)。
计算机中为什么用补码进行运算(用的是补码存储)?
答:如果用原码表示,让符号位也参与计算,显然对于减法来说,结果是不正确的。这也就是为何计算机内部不使用原码表示一个数。为了解决原码做减法的问题, 出现了反码。发现用反码计算减法,结果的真值部分是正确的。而唯一的问题其实就出现在"0"这个特殊的数值上,虽然人们理解上+0和-0是一样的,但是0带符号是没有任何意义的,而且会有[0000 0000]原和[1000 0000]原两个编码表示0。于是补码的出现,解决了0的符号问题以及0的两个编码问题:1-1 = 1 + (-1) = [0000 0001]原+ [1000 0001]原= [0000 0001]补+ [1111 1111]补= [1 0000 0000]补=[0000 0000]补=[0000 0000]原注意:进位1不在计算机字长里。
这样0用[0000 0000]表示,而以前出现问题的-0则不存在了。而且可以用[1000 0000]表示-128:-128的由来如下:
(-1) + (-127) = [1000 0001]原+ [1111 1111]原= [1111 1111]补+ [1000 0001]补= [1000 0000]补
-1-127的结果应该是-128,在用补码运算的结果中,[1000 0000]补就是-128,但是注意因为实际上是使用以前的-0的补码来表示-128,所以-128并没有原码和反码表示。(对-128的补码表示[1000 0000]补,算出来的原码是[0000 0000]原,这是不正确的)
使用补码,不仅仅修复了0的符号以及存在两个编码的问题,而且还能够多表示一个最低数。这就是为什么8位二进制,使用原码或反码表示的范围为[-127, +127],而使用补码表示的范围为[-128, 127]。
因为机器使用补码,所以对于编程中常用到的有符号的32位int类型,可以表示范围是: [-231, 231-1] 因为第一位表示的是符号位,而使用补码表示时又可以多保存一个最小值
读锁写锁的表示
进入正题,源码分析:
在读写锁中,用int类型变量的state表示读锁数量和写锁数量。int类型变量是32位。其中高16位表示读锁数量;低16位表示写锁数量。
static final int SHARED_SHIFT = 16; // 此值为65536 static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 此值为65526-1=65525,表示16位数能表示的最大值。 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
获取当前读锁数量的方法是:
/** >>>表示无符号右移。在>>这个运算符中,如果这个数为正数,向右移位,并且前面补对应位数的0;如果这个数为负*数,向右移位,并且前面补对应位数的1;而‘<<*此条代码的意思为,获取当前的state,当做参数c传入此方法,获取c的高16位的值*/static int sharedCount(int c) { return c >>> SHARED_SHIFT; }// 读锁加1的方法是state+SHARED_UNIT,表示在state的前16位上+1// 读锁减1的方法是state-SHARED_UNIT,表示在state的前16位上-1
获取当前写锁数量的方法是:
/***EXCLUSIVE_MASK的值为65535,为16位数所能表示的最大值,二进制表示为1111111111111111(其实前面还有16个*0)。state&65535,的意义是获取state的后16位的值。获取的是写锁*/static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }// 写锁+1的方法是state+加锁数量。直接加就行,因为低16位表示写锁// 写锁-1的方法是state-加锁数量。直接减就行,因为低16位表示写锁
ReentrantReadWriteLock类结构图
ReentrantReadWriteLock的构造方法
// 在调用构造方法时,传入boolean类型的参数,公平锁还是非公平锁public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); }// sync的构造函数Sync() { readHolds = new ThreadLocalHoldCounter(); setState(getState()); // ensures visibility of readHolds }protected ReadLock(ReentrantReadWriteLock lock) { sync = lock.sync; }protected WriteLock(ReentrantReadWriteLock lock) { sync = lock.sync; }
读锁的获取过程
public void lock() { sync.acquireShared(1); }public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }// tryAcquireShared(arg)有多种实现,下面研究在读写锁中的实现protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); // 判断当前写锁数量是否为0,如果不为0并且不是持有写锁的线程重入,直接返回-1,去进行阻塞;如果 // 是持有写锁的线程重入或者写锁此时没被占用,则进入下面的逻辑。 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; int r = sharedCount(c);// 获取此时读锁数量 // 判断当前线程是否应该阻塞,此方法在公平锁和非公平锁各有一个实现。如果发现当前线程不用阻塞 // 并且读锁还没超限并且CAS修改state成功,进入此逻辑块 if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { if (r == 0) { // 如果此时读锁数为0,表示此时没有线程拿到读锁 firstReader = current;// 设置当前线程为firstReader(用来保存第一个拿锁线程) firstReaderHoldCount = 1;// 设置第一个拿锁线程的计数器为1 // 如果此时读锁数不为0,但是当前线程是第一个拿到读锁的线程,表示正在重入 } else if (firstReader == current) { firstReaderHoldCount++; // 则让第一个读线程计数器+1 // 如果此时读锁数不为0,并且也不是第一个拿到读锁线程的重入,则进入else块 } else { // 获取在当前状态下最后一个拿到读锁的线程的计数器 HoldCounter rh = cachedHoldCounter; // 如果此时计数器为null,说明当前线程是第二个线程,HoldCounter还没有被初始化; // 如果此时计数器不为null,但是计数器里的一个属性tid不是当前线程id。此时表示计数 // 器已经被初始化,但是当前线程并不是最后一个拿到读锁线程的重入,此时就应该调用 // ThreadLocal中的get()方法,看一看在当前线程的Thread类中的 // threadLocals(ThreadLocalMap类型)中是否保存了以这个读写锁中的ThreadLocal // 为key,HoldCounter为value的键值对 if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); // 如果rh不为null,并且是最后一个拿到读锁线程的重入,但是此时他的读锁计数器为0 // 代表:可能在重入的过程中,他经历了一次释放,释放会remove。需要再次set else if (rh.count == 0) // 就再次把读写锁中的这个ThreadLocal变量(ThreadLocalHoldCounter这个值, // 这个值在构造读写锁的时候被初始化)当做key,计数器当做vlaue,再次存到 // Thread中的threadLocals中 readHolds.set(rh); // 不管咋样,最后记得把当前计数器的计数+1,表示拿到了读锁 rh.count++; } // 返回1,表示拿到读锁成功 return 1; } // 什么时候会进入到这个方法呢?1.当前线程应当阻塞 2.读锁数目超出限制 3.CAS修改state失败 return fullTryAcquireShared(current); }// 公平锁模式下的实现,调用的是hasQueuedPredecessors方法, 之前介绍过不在赘述,判断当前线程用不用排队final boolean readerShouldBlock() { return hasQueuedPredecessors(); }// 非公平锁模式下的实现,调用下面这个方法final boolean readerShouldBlock() { return apparentlyFirstQueuedIsExclusive(); } /** *如果同步队列已经初始化并且至少有两个节点并且第一个等待的线程为写线程并且这个写线程不为空,则返回true *如果其中有一个条件出现问题就返回false,表示此线程不需要阻塞。 *需要注意的是线程重入可能会被判定为true */ final boolean apparentlyFirstQueuedIsExclusive() { Node h, s; return (h = head) != null && (s = h.next) != null && !s.isShared() && s.thread != null; } /** *1.当前线程应当阻塞 2.读锁数目超出限制 3.CAS修改state失败 *以上三种情况会进入此方法 */ final int fullTryAcquireShared(Thread current) { // 声明一个变量计数器rh HoldCounter rh = null; // 进入自旋 for (;;) { int c = getState(); // 如果此时独占锁被占用,进入下面逻辑 if (exclusiveCount(c) != 0) { // 如果此时独占锁的线程不是当前线程,直接返回-1。 // 如果是当前线程,进入下面逻辑 // 如果没有这个if判断,线程会直接进入else if判断,如果当前线程是写锁线程重入获取 // 读锁,但是他在之前方法进行readerShouldBlock判断时,有另一个写锁线程排队排在 // 他前面了,他就会进入fullTryAcquireShared方法,这个时候在进入下面的 // readerShouldBlock方法还是会判定为真,如果他在后续的代码中返回-1,则当前线程 // 会进入阻塞中,但是当前线程是独占写锁的,他沉睡了,不会被唤醒,其他想要获得锁(不 // 管读锁还是写锁)的线程都会获取不到,形成死锁。 if (getExclusiveOwnerThread() != current) return -1; // 如果此时写锁没被占用,并且当前线程在进行判定时,还需要阻塞 } else if (readerShouldBlock()) { // 看看此时是不是首个获取读锁的线程的重入,如果是,进入下面逻辑。 if (firstReader == current) { // assert firstReaderHoldCount > 0; // 如果此时并不是首个读锁线程的重入,此时可能是其他获取读锁线程的重入,也可能是其他 // 获取读锁线程的重入 } else { if (rh == null) { // 判断此时的rh是否为null,如果为null,让rh等于最后获取读锁线程的计数器 rh = cachedHoldCounter; // 如果此时的rh为null(可能当前线程是第二个来获取读锁的线程)或者不是最后 // 一个获取读锁线程的重入,则进入下面逻辑 if (rh == null || rh.tid != getThreadId(current)) { // 拿到此线程对应的读锁计数器,如果没有计数器get方法内部会new一个 rh = readHolds.get(); // 如果此rh计数为0,说明他是第一次来竞争读锁的(之前没拿到过读锁)或 // 者是已经拿到过锁的线程释放过锁了,计数变为0 if (rh.count == 0) // 把线程Thread类中对应的threadlocals中的数据移除掉,省着浪 // 费空间 readHolds.remove(); } } // 如果说走到这步,判定出计数器的计数为0,说明此线程是刚来的或者是之前获取到读 // 锁的线程在这个过程中释放过一次锁了,那你自己已经释放过了,肯定不能再让你获 // 取把,返回-1,乖乖排队阻塞。 if (rh.count == 0) return -1; } } // 如果走到了这里,OK。你现在已经在获取读锁的路上完成了百分之九十,你接下来要做的就是把 // 线程读锁的数量、计数器的值更新一下就行了 // 如果此时的读锁数量已经等于最大值了,没办法在多获取一个了,返回错误 if (sharedCount(c) == MAX_COUNT) throw new Error("Maximum lock count exceeded"); // 尝试着用CAS的方式去修改读锁数量,修改成功,进入下面代码;修改不成功,继续自旋 if (compareAndSetState(c, c + SHARED_UNIT)) { // 如果此时读锁数量为0,firstReader指向为当前线程 // 如下的几个if判断是为了判断没有满足上面几种情况的其他情况 if (sharedCount(c) == 0) { firstReader = current; firstReaderHoldCount = 1; // 如果是第一个获取读锁线程的重入,计数+1 } else if (firstReader == current) { firstReaderHoldCount++; // 如果是其他获取读锁线程的重入或者是首次来竞争读锁的线程 } else { if (rh == null) rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); // 如果此时rh不为null,并且是最后一个获取读锁线程的重入,并且此时count的值为 // 0,说明在这个过程中,进行释放操作了,释放操作会remove操作,此时因为他又来 // 竞争锁了。这个情况的前提是,在最后一个获取读锁的线程进行释放锁以后,并没有 // 其他线程来获取读锁了。此时重新把计数器保存在当前线程的threadlocals中 else if (rh.count == 0) readHolds.set(rh); // 不管怎么样,最后要把计数器的值+1,表示此次获取到读锁了,并且把最后获取到读 // 锁的线程指向为当前线程 rh.count++; cachedHoldCounter = rh; // cache for release } // 获取读锁成功 return 1; } } }
读锁的释放过程
public void unlock() { sync.releaseShared(1); }public final boolean releaseShared(int arg) { // tryReleaseShared方法有多种实现,这个方法是在读写锁中的实现 if (tryReleaseShared(arg)) { doReleaseShared();// 唤醒第一个等待的线程 return true; } return false; }protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); // 判断当前要释放读锁的线程是不是firstReader中保存的线程。也就是说是不是首个拿到读锁线程 if (firstReader == current) { // assert firstReaderHoldCount > 0; // 如果是首个拿到读锁的线程,并且计数为1 if (firstReaderHoldCount == 1) // 把变量置为null,这里为什么没有把计数也置为0,我感觉这里无关紧要 firstReader = null; else // 如果此时第一个拿到读锁的线程不为1,就让他的计数-1 firstReaderHoldCount--; } else { // 如果此时释放锁的线程并不是此时第一个获取读锁的线程,则进入下面模块 // 拿到最后一个获取读锁的线程的计数器 HoldCounter rh = cachedHoldCounter; // 如果此时的rh变量为空。(这为什么会为空呢,之前说不是第一个获取线程的重入吗,肯定会存在线程计数器的啊。我感觉这里是作者偷懒了)或者rh不为空,但是不是最后一个获取读锁线程的重入 if (rh == null || rh.tid != getThreadId(current)) // 拿到当前线程对应的锁计数器 rh = readHolds.get(); // 到这里说明是最后一个获取读锁线程的重入或者是上面逻辑走到这里的,获取计数器的计数值 int count = rh.count; // 如果此时计数器的count小于等于1,说明释放此锁后,此线程也对读锁进行了释放 if (count <= 1) { // 就把此线程里负责保存线程计数器的threadLocals变量里的内容清空,节省空间 readHolds.remove(); // 如果说此时的count小于等于0,则抛出不匹配解锁异常,你根本没拿到过锁,过来凑什么 // 热闹(爬).这也是上文中,我说偷懒的原因,我感觉完全也可以在上文中直接抛出异常 if (count <= 0) throw unmatchedUnlockException(); } --rh.count; // 将计数器的计数值-1 } for (;;) { //自旋操作 int c = getState();// 拿到当前state的值 int nextc = c - SHARED_UNIT;// 对state值读锁数量部分-1 // 如果CAS更改state的值成功,返回更改之后的state的值是否等于0 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,会唤醒等待的写锁(正常使用 // 情况下也不可能会出现读锁是第一个等待节点的情况) return nextc == 0; } }