6.JUC之ReentrantReadWriteLock
一、概述:
Java纪年1.5年,ReentrantReadWriteLock诞生于JUC,此后,国人一般称它为读写锁。人如其名,他就是一个可重入锁,同时他还是一个读写锁
a)跟ReentrantLock并没有任何的亲属关系
因为ReentrantReadWriteLock在命名上跟ReentrantLock非常贴近,很容易让人认为他跟ReentrantLock有继承关系,其实并没有。ReentrantReadWriteLock 实现了 ReadWriteLock 和 Serializable,同时 ReadWriteLock 跟 Lock 也没有继承关系,
ReadWriteLock 是独立的一个接口,维护了一对相关的锁
,一个用于只读操作,另一个用于写入操作,只要没有 writer,读取锁
可以由多个 reader 线程同时保持。写入锁
是独占的。
ReentrantReadWriteLock 跟 ReentrantLock 只有朋友关系,他们都是 可重入锁
但是ReentrantReadWriteLock 的重入递归层级只有 65535,即读锁能递归65535、写锁也同样能够递归65535层,至于为何是65535呢?在AQS框架的时候说过,AQS是用一个Integer来表示锁的状态。而一个Integer有32位,读锁用一半,写锁用一半,16bit = 65535
b)ReentrantReadWriteLock也有公平性
ReentrantReadWriteLock除了和ReentrantLock一样具有可重入性之外,他们也都具有公平性。既他们都有公平锁和非公平锁的实现。实现方式也差不太远,关于公平性的内容可以查看上一篇博客
二、ReentrantReadWriteLock中的读锁与写锁
ReentrantReadWriteLock 提供一个读写分离的锁,读锁由ReadLock控制,写锁由WriteLock完成。当然读与写是互斥的。如你所知,可读不写,可写不读,即是读写不能同时进行,这就是读写锁
。之所以能做到读写互斥说明他们最终还是用了同一个同步器(Sync),他们依赖于上层(ReentrantReadWriteLock)的同步器,Sync只有一个,所以读锁与写锁不能同时使用
通过查看源码,我们可以得到ReentrantReadWriteLock的以下几个特点:
1.当读锁被持有,不管是被一人持有,还是多人持有,写都需要阻塞。
2.当写锁被持有,当然只有一人能持有(独占), 读锁将会被阻塞
3.读写锁的阻塞方式直接由公平性决定,由FairSync 或 NonFairSync实现
4.读锁可以有多人同时持有,HoldCounter的作用就是当前线程持有共享锁的数量
ReentrantReadWriteLock 中的 WriteLock写锁的获取和释放过程和 ReentrantLock 几乎相同,都是独占排他锁,都是使用了AQS的acquire/release操作
ReadLock读锁就有区别了,前面我们提到 AQS提供了几个抽象方法让子类去实现,其中就有 tryAcquireShared 和 tryReleaseShared,用于共享锁的获取和释放,所以我们这里重点关注下 读锁的获取和释放过程:
读取的获取:
1 protected final int tryAcquireShared(int unused) { 2 Thread current = Thread.currentThread(); 3 //锁的持有线程数 4 int c = getState(); 5 /* 6 * 如果写锁线程数 != 0 ,且独占锁不是当前线程则返回失败,因为存在锁降级 7 */ 8 if (exclusiveCount(c) != 0 && 9 getExclusiveOwnerThread() != current) 10 return -1; 11 //读锁线程数 12 int r = sharedCount(c); 13 /* 14 * readerShouldBlock():读锁是否需要等待(公平锁原则) 15 * r < MAX_COUNT:持有线程小于最大数(65535) 16 * compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态 17 */ 18 if (!readerShouldBlock() && r < MAX_COUNT && 19 compareAndSetState(c, c + SHARED_UNIT)) { 20 /* 21 * holdCount部分后面讲解 22 */ 23 if (r == 0) { 24 firstReader = current; 25 firstReaderHoldCount = 1; 26 } else if (firstReader == current) { 27 firstReaderHoldCount++; // 28 } else { 29 HoldCounter rh = cachedHoldCounter; 30 if (rh == null || rh.tid != current.getId()) 31 cachedHoldCounter = rh = readHolds.get(); 32 else if (rh.count == 0) 33 readHolds.set(rh); 34 rh.count++; 35 } 36 return 1; 37 } 38 return fullTryAcquireShared(current); 39 }
读锁的获取过程分析:
1.如果写线程持有锁(也就是独占锁数量不为0),并且独占线程不是当前线程(为什么还要满足这个条件,是为了实现 锁降级),那么读取失败
2.如果读线程请求锁数量达到了 65535(包括重入的部分)(state),那么就抛出异常
3.如果读线程不用等待(实际上是 是否需要公平锁),并且增加读取锁状态数成功,那么就返回成功,否则执行下一步
4.上一步失败的原因是 CAS操作修改状态数失败,那么就需要循环不断尝试去修改状态直到成功 或者 锁被写入线程占有
HoldCounter
前面我们提到,读锁可以有多人同时持有,HoldCounter的作用就是记录当前线程持有共享锁的数量(不是记录所有读线程的共享锁数量,那个由state去记录),下面我们看看 HoldCounter是什么东西,又是如何完成计数的
首先我们看到只有在获取共享锁(读锁)的时候 + 1,也只有在释放共享锁的时候 - 1 ,会起作用
强调一下,对于共享锁,其实并不是锁的概念,更像是计数器的概念。一个共享锁就相对于一个计数器操作,一次获取共享锁相当于计数器 + 1,释放一个共享锁相当于计数器 - 1.显然只有线程持有了共享锁(也就是当前线程携带一个计数器,描述自己持有多少个共享锁或者多重共享锁),才能释放一个共享锁。否则一个没有获取共享锁的线程调用一次释放操作就会导致读写锁的state(持有的线程数,包括重入数)错误
先看读锁获取锁的部分:
1 if (r == 0) { //r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中 2 firstReader = current; 3 firstReaderHoldCount = 1; 4 } else if (firstReader == current) { //第一个读锁线程重入 5 firstReaderHoldCount++; 6 } else { //非firstReader计数 7 HoldCounter rh = cachedHoldCounter; //readHoldCounter缓存 8 //rh == null 或者 rh.tid != current.getId(),需要获取rh 9 if (rh == null || rh.tid != current.getId()) 10 cachedHoldCounter = rh = readHolds.get(); 11 else if (rh.count == 0) 12 readHolds.set(rh); //加入到readHolds中 13 rh.count++; //计数+1 14 }
HoldCounter的定义:
1 static final class HoldCounter { 2 int count = 0; 3 final long tid = Thread.currentThread().getId(); 4 }
在HoldCounter中仅有count和tid两个变量,其中count代表着计数器,tid是线程的id。但是如果要将一个对象和线程绑定起来仅记录tid肯定不够的,而且HoldCounter根本不能起到绑定对象的作用,只是记录线程tid而已。
诚然,在java中,我们知道如果要将一个线程和对象绑定在一起只有ThreadLocal才能实现。所以如下:
1 static final class ThreadLocalHoldCounter 2 extends ThreadLocal<HoldCounter> { 3 public HoldCounter initialValue() { 4 return new HoldCounter(); 5 } 6 }
故而,HoldCounter应该就是绑定线程上的一个计数器,而ThradLocalHoldCounter则是线程绑定的ThreadLocal。从上面我们可以看到ThreadLocal将HoldCounter绑定到当前线程上,同时HoldCounter也持有线程Id,这样在释放锁的时候才能知道ReadWriteLock里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get()的次数,因为这也是一个耗时操作。需要说明的是这样HoldCounter绑定线程id而不绑定线程对象的原因是避免HoldCounter和ThreadLocal互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助GC快速回收对象而已
四、总结
关于ReentrantReadWriteLock里面的内容还有很多,有时间还可以去看看源码去品一品,
在Java1.8之前,它是JDK实现的读写锁(ReadWriteLock)的唯一实现。他由读、写锁两部分组成,写是独占锁,而读是共享锁,且读写互斥
ReentrantReadWriteLock和ReentrantLock并无关系,但是他们有很多类似的地方,比如都具有可重入性、都有两种获取锁的策略:公平与非公平,与ReentrantLock一样在非公平模式能获得更高的OPS