ReentrantReadWriteLock解析
前文提到的synchronized关键和和ReentrantLock,它们都是独占式锁,排他锁。在同一时刻只能有一个线程获取多。这个就非常不适合那种读多写少的场景。
如果有多个线程需要读取共享数据,极少数甚至只有一个线程写共享数据的话,就非常不划算了。读操作对数据没有影响,完全可以并发进行。
于是Java提供了另外一个实现了Lock接口的ReentrantReadWriteLock(可重入读写锁)。使用这个锁时,多个读线程可以在同一个时刻访问共享资源。但是在写线程访问的时候,所有的读线程和其他写线程都会被阻塞。
它还有以下特点:
- 支持公平锁和非公平锁,默认非公平锁
- 可重入。不管是读锁还是写锁,线程在获取之后,还能再次获取。写锁在成功获取之后,也能获取读锁。
- 锁降级:遵循获取写锁,获取读锁,然后释放写锁的次序,写锁就能降级为读锁。
类结构
请见下图
在分别分析读写锁之前,我们看下读写锁在Sync中时怎么计数的。
在ReentrantReadWriteLock的内部抽象静态类中有这么几句
1 | /* |
其中方法sharedCount是用作获取读锁被获取的次数。它将同步状态c右移16位,取它的高16位。
方法exclusiveCount是用作获取写锁被获取的次数。EXCLUSIVE_MASK为1左移16位,然后减1,即为0x0000FFFF。然后和当前的同步状态C相与,获取同步状态的低16位。
示意图如下:
写锁
写锁的获取
其他流程已经在AQS里面实现了,我们具体看一下写锁的tryAcquire方法
1 | protected final boolean tryAcquire(int acquires) { |
大体流程是:
- 如果同步状态c不为0,写锁被获取次数为0,说明此时有线程已经获取到了读锁,获取失败
- 亦或同步状态c不为0,当前线程不是获取写锁的线程,获取失败
- 亦或持有的写锁次数应超过最大可持有数目了。这里写锁只可有一个线程持有,但是可以重入MAX_COUNT次
- 如果上述情况均没有,则当前线程可获取写锁,设置同步状态,设置独占线程为线程本身
注意到还有一个writerShouldBlock方法,这个方法在公平锁和非公平锁中的实现逻辑是不一样的。
在公平锁中
1 | final boolean writerShouldBlock() { |
它是以队列里面是否有正在等候的线程来判断的。
而非公平锁中直接返回false,因为非公平锁是支持抢占的
1 | final boolean writerShouldBlock() { |
写锁的释放
1 | /* |
流程基本上和ReentrantLock差不多,因为写锁是同步状态的低16位表示的,所以,直接用getState()-releases就行了。
读锁
读锁和写锁不一样,它不是独占的,排他的,它是一种共享锁。同一时刻可以被多个线程获取。按照上一篇AQS文章中的介绍,读锁需要重写AQS中的tryAcquireShared和tryReleaseShared方法。
读锁的获取
1 | protected final int tryAcquireShared(int unused) { |
JDK源码自带的注释已经说得很清楚了。
- 如果写锁被其他线程获取了,获取读锁失败
- 否则,获取读锁成功,更新同步状态,只更新同步状态c的高16位的值
- 无论是CAS失败或者同一线程再次获取读锁时,都会调用fullTryAcquireShared方法
fullTryAcquireShared方法
1 | /** |
代码和tryAcquireShared类似。就是一个dead loop,不断去尝试设置同步状态。
读锁的释放
1 | protected final boolean tryReleaseShared(int unused) { |
锁降级
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
可以看看官方文档对锁降级的示例代码
1 | class CachedData { |
在释放写锁前,需要先获得读锁,然后再释放写锁。如果不先获取读锁,那么其他线程在这个线程释放写锁后可能会修改data,而这种修改对于这个线程是不可见的,从而在之后的use(data)中使用的是错误的值 。