ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。
所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。ReadWriteLock适用于读多写少的并发情况。
Java并发包中ReadWriteLock是一个接口,主要有两个方法,如下:
1 | public interface ReadWriteLock { |
Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。
特性
ReentrantReadWriteLock有如下特性:
- 获取顺序
- 非公平模式(默认)
当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。 - 公平模式
当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。
当有写线程持有写锁或者有等待的写线程时,一个尝试获取公平的读锁(非重入)的线程就会阻塞。这个线程直到等待时间最长的写锁获得锁后并释放掉锁后才能获取到读锁。
- 非公平模式(默认)
- 可重入
允许读锁可写锁可重入。写锁可以获得读锁,读锁不能获得写锁。 - 锁降级
允许写锁降低为读锁 - 中断锁的获取
在读锁和写锁的获取过程中支持中断 - 支持Condition
写锁提供Condition实现 - 监控
提供确定锁是否被持有等辅助方法
使用
下面一段代码展示了锁降低的操作:
1 | class CachedData { |
ReentrantReadWriteLock可以用来提高某些集合的并发性能。当集合比较大,并且读比写频繁时,可以使用该类。下面是TreeMap使用ReentrantReadWriteLock进行封装成并发性能提高的一个例子:
1 | class RWDictionary { |
源码分析
构造方法
ReentrantReadWriteLock有两个构造方法,如下:
1 |
|
可以看到,默认的构造方法使用的是非公平模式,创建的Sync是NonfairSync对象,然后初始化读锁和写锁。一旦初始化后,ReadWriteLock接口中的两个方法就有返回值了,如下:
1 | public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; } |
从上面可以看到,构造方法决定了Sync是FairSync还是NonfairSync。Sync继承了AbstractQueuedSynchronizer,而Sync是一个抽象类,NonfairSync和FairSync继承了Sync,并重写了其中的抽象方法。
Sync分析
Sync中提供了很多方法,但是有两个方法是抽象的,子类必须实现。下面以FairSync为例,分析一下这两个抽象方法:
1 | static final class FairSync extends Sync { |
writerShouldBlock和readerShouldBlock方法都表示当有别的线程也在尝试获取锁时,是否应该阻塞。
对于公平模式,hasQueuedPredecessors()方法表示前面是否有等待线程。一旦前面有等待线程,那么为了遵循公平,当前线程也就应该被挂起。
下面再来看NonfairSync的实现:
1 | static final class NonfairSync extends Sync { |
从上面可以看到,非公平模式下,writerShouldBlock直接返回false,说明不需要阻塞;而readShouldBlock调用了apparentFirstQueuedIsExcluisve()方法。该方法在当前线程是写锁占用的线程时,返回true;否则返回false。也就说明,如果当前有一个写线程正在写,那么该读线程应该阻塞。
继承AQS的类都需要使用state变量代表某种资源,ReentrantReadWriteLock中的state代表了读锁的数量和写锁的持有与否,整个结构如下:
可以看到state的高16位代表读锁的个数;低16位代表写锁的状态。
获取锁
读锁的获取
当需要使用读锁时,首先调用lock方法,如下:
1 | public void lock() { |
从代码可以看到,读锁使用的是AQS的共享模式,AQS的acquireShared方法如下:
1 | if (tryAcquireShared(arg) < 0) |
当tryAcquireShared()方法小于0时,那么会执行doAcquireShared方法将该线程加入到等待队列中。
Sync实现了tryAcquireShared方法,如下:
1 | protected final int tryAcquireShared(int unused) { |
从上面的代码以及注释可以看到,分为三步:
- 如果当前有写线程并且本线程不是写线程,那么失败,返回-1
- 否则,说明当前没有写线程或者本线程就是写线程(可重入),接下来判断是否应该读线程阻塞并且读锁的个数是否小于最小值,并且CAS成功使读锁+1,成功,返回1。其余的操作主要是用于计数的
- 如果2中失败了,失败的原因有三,第一是应该读线程应该阻塞;第二是因为读锁达到了上线;第三是因为CAS失败,有其他线程在并发更新state,那么会调动fullTryAcquireShared方法。
fullTryAcquiredShared方法如下:
1 | final int fullTryAcquireShared(Thread current) { |
从上面可以看到fullTryAcquireShared与tryAcquireShared有很多类似的地方。
在上面可以看到多次调用了readerShouldBlock方法,对于公平锁,只要队列中有线程在等待,那么将会返回true,也就意味着读线程需要阻塞;对于非公平锁,如果当前有线程获取了写锁,则返回true。一旦不阻塞,那么读线程将会有机会获得读锁。
写锁的获取
写锁的lock方法如下:
1 | public void lock() { |
AQS的acquire方法如下:
1 | public final void acquire(int arg) { |
从上面可以看到,写锁使用的是AQS的独占模式。首先尝试获取锁,如果获取失败,那么将会把该线程加入到等待队列中。
Sync实现了tryAcquire方法用于尝试获取一把锁,如下:
1 | protected final boolean tryAcquire(int acquires) { |
从代码和注释可以看到,获取写锁时有三步:
- 如果当前有写锁或者读锁。如果只有读锁,返回false,因为这时如果可以写,那么读线程得到的数据就有可能错误;如果有写锁,但是线程不同,即不符合写锁重入规则,返回false
- 如果写锁的数量将会超过最大值65535,抛出异常;否则,写锁重入
- 如果没有读锁或写锁的话,如果需要阻塞或者CAS失败,返回false;否则将当前线程置为获得写锁的线程
从上面可以看到调用了writerShouldBlock方法,FairSync的实现是如果等待队列中有等待线程,则返回false,说明公平模式下,只要队列中有线程在等待,那么后来的这个线程也是需要记入队列等待的;NonfairSync中的直接返回的直接是false,说明不需要阻塞。从上面的代码可以得出,当没有锁时,如果使用的非公平模式下的写锁的话,那么返回false,直接通过CAS就可以获得写锁。
总结
从上面分析可以得出结论:
- 如果当前没有写锁或读锁时,第一个获取锁的线程都会成功,无论该锁是写锁还是读锁。
- 如果当前已经有了读锁,那么这时获取写锁将失败,获取读锁有可能成功也有可能失败
- 如果当前已经有了写锁,那么这时获取读锁或写锁,如果线程相同(可重入),那么成功;否则失败
释放锁
获取锁要做的是更改AQS的状态值以及将需要等待的线程放入到队列中;释放锁要做的就是更改AQS的状态值以及唤醒队列中的等待线程来继续获取锁。
读锁的释放
ReadLock的unlock方法如下:
1 | public void unlock() { |
调用了Sync的releaseShared方法,该方法在AQS中提供,如下:
1 | public final boolean releaseShared(int arg) { |
调用tryReleaseShared方法尝试释放锁,如果释放成功,调用doReleaseShared尝试唤醒下一个节点。
AQS的子类需要实现tryReleaseShared方法,Sync中的实现如下:
1 | protected final boolean tryReleaseShared(int unused) { |
从上面可以看到,释放锁的第一步是更新firstReader或HoldCounter的计数,接下来进入死循环,尝试更新AQS的状态,一旦更新成功,则返回;否则,则重试。
释放读锁对读线程没有影响,但是可能会使等待的写线程解除挂起开始运行。所以,一旦没有锁了,就返回true,否则false;返回true后,那么则需要释放等待队列中的线程,这时读线程和写线程都有可能再获得锁。
写锁的释放
WriteLock的unlock方法如下:
1 | public void unlock() { |
Sync的release方法使用的AQS中的,如下:
1 | public final boolean release(int arg) { |
调用tryRelease尝试释放锁,一旦释放成功了,那么如果等待队列中有线程再等待,那么调用unparkSuccessor将下一个线程解除挂起。
Sync需要实现tryRelease方法,如下:
1 | protected final boolean tryRelease(int releases) { |
从上面可以看到,写锁的释放主要有三步:
- 如果当前没有线程持有写锁,但是还要释放写锁,抛出异常
- 得到解除一把写锁后的状态,如果没有写锁了,那么将AQS的线程置为null
- 不管第二步中是否需要将AQS的线程置为null,AQS的状态总是要更新的
从上面可以看到,返回true当且只当没有写锁的情况下,还有写锁则返回false。
总结
从上面的分析可以得出:
- 如果当前是写锁被占有了,只有当写锁的数据降为0时才认为释放成功;否则失败。因为只要有写锁,那么除了占有写锁的那个线程,其他线程即不可以获得读锁,也不能获得写锁
- 如果当前是读锁被占有了,那么只有在写锁的个数为0时才认为释放成功。因为一旦有写锁,别的任何线程都不应该再获得读锁了,除了获得写锁的那个线程。
其他方法
看完了ReentrantReadWriteLock中的读锁的获取和释放,写锁的获取和释放,再来看一下其余的一些辅助方法来加深我们对读写锁的理解。
getOwner()
getOwner方法用于返回当前获得写锁的线程,如果没有线程占有写锁,那么返回null。实现如下:
1 | protected Thread getOwner() { |
可以看到直接调用了Sync的getOwner方法,下面是Sync的getOwner方法:
1 | final Thread getOwner() { |
如果独占锁的个数为0,说明没有线程占有写锁,那么返回null;否则返回占有写锁的线程。
getReadLockCount()
getReadLockCount()方法用于返回读锁的个数,实现如下:
1 | public int getReadLockCount() { |
Sync的实现如下:
1 | final int getReadLockCount() { |
从上面代码可以看出,要想得到读锁的个数,就是看AQS的state的高16位。这和前面讲过的一样,高16位表示读锁的个数,低16位表示写锁的个数。
getReadHoldCount()
getReadHoldCount()方法用于返回当前线程所持有的读锁的个数,如果当前线程没有持有读锁,则返回0。直接看Sync的实现即可:
1 | final int getReadHoldCount() { |
从上面的代码中,可以看到两个熟悉的变量,firstReader和HoldCounter类型。这两个变量在读锁的获取中接触过,前面没有细说,这里细说一下。HoldCounter类的实现如下:
1 | static final class HoldCounter { |
readHolds是ThreadLocalHoldCounter类,定义如下:
1 | static final class ThreadLocalHoldCounter |
可以看到,readHolds存储了每一个线程的HoldCounter,而HoldCounter中的count变量就是用来记录线程获得的写锁的个数。所以可以得出结论:Sync维持总的读锁的个数,在state的高16位;由于读线程可以同时存在,所以每个线程还保存了获得的读锁的个数,这个是通过HoldCounter来保存的。
除此之外,对于第一个读线程有特殊的处理,Sync中有如下两个变量:
1 | private transient Thread firstReader = null; |
看完了HoldCounter和firstReader,再来看一下getReadLockCount的实现,主要有三步:
- 当前没有读锁,那么自然每一个线程获得的读锁都是0;
- 如果当前线程是第一个获取到读锁的线程,那么返回firstReadHoldCount;
- 如果当前线程不是第一个获取到读锁的线程,得到该线程的HoldCounter,然后返回其count字段。如果count字段为0,说明该线程没有占有读锁,那么从readHolds中移除。获取HoldCounter分为两步,第一步是与cachedHoldCounter比较,如果不是,则从readHolds中获取。
getWriteLockCount()
getWriteLockCount()方法返回写锁的个数,Sync的实现如下:
1 | final int getWriteHoldCount() { |
可以看到如果没有线程持有写锁,那么返回0;否则返回AQS的state的低16位。
总结
当分析ReentranctReadWriteLock时,或者说分析内部使用AQS实现的工具类时,需要明白的就是AQS的state代表的是什么。ReentrantLockReadWriteLock中的state同时表示写锁和读锁的个数。为了实现这种功能,state的高16位表示读锁的个数,低16位表示写锁的个数。AQS有两种模式:共享模式和独占模式,读写锁的实现中,读锁使用共享模式;写锁使用独占模式;另外一点需要记住的即使,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁。