概述
ReadWriteLock,顾名思义,是读写锁。它维护了一对相关的锁 — — “读取锁”和“写入锁”,一个用于读取操作,另一个用于写入操作。
- “读取锁”用于只读操作,它是“共享锁”,能同时被多个线程获取。
- “写入锁”用于写入操作,它是“独占锁”,写入锁只能被一个线程锁获取。
注意:不能同时存在读取锁和写入锁!
ReadWriteLock是一个接口。ReentrantReadWriteLock是它的实现类,ReentrantReadWriteLock包括子类ReadLock和WriteLock。
读写锁底层也是通过AQS框架实现的,通过之前的文章可知,如果使用AQS框架,在这个类内部必定有一个AQS框架的子类,AQS子类无非就是实现如下几点:第一点:对共享资源state进行操作
第二点:如果是独占模式下对资源的获取和释放,那么必须实现tryAcquire()方法和tryRelease()方法。
第三点:如果是共享模式下对资源的获取和释放,那么必须实现tryAcquireShared()方法和tryReleaseShared()方法。
而读写锁ReentrantReadWriteLock即包含独占模式下资源的获取和释放(写锁),也包含共享模式下资源的获取和释放(读锁),所以需要上面的四个方法都需要实现。
用state一个整形变量怎样去维护读和写两种状态
共享资源state是一个整形变量,在读写锁中又必须维护读和写两种状态,所以只能按位把state分割成两个部分,一个整形有32位,高16位代表读,低16位代表写。如图所示:
![img][link1]
1 | //读状态占用位数 |
读操作,因为高16位为读操作,要想获取读的数量,只需将state无符号右移16位即可:state>>>16。读最大的数量:65535(二进制表示:11111111110000000000000000)
写操作,因为低16位为写操作,要向获取写数量,只需要把state的高16位抹去就可以了,很容易想到“按位与”操作:state&1111111111111111。写最大的数量:65535(二进制表示:00000000000000001111111111111111)
共享锁
1 | public static class ReadLock implements Lock, java.io.Serializable { |
ReadLock中的sync是一个Sync对象,Sync继承于AQS类,即Sync就是一个锁。ReentrantReadWriteLock中也有一个Sync对象,而且ReadLock中的sync和ReentrantReadWriteLock中的sync是对应关系。即ReentrantReadWriteLock和ReadLock共享同一个AQS对象,共享同一把锁。
获取共享锁
获取共享锁的思想(即lock函数的步骤),是先通过tryAcquireShared()尝试获取共享锁。尝试成功的话,则直接返回;尝试失败的话,则通过doAcquireShared()不断的循环并尝试获取锁,若有需要,则阻塞等待。doAcquireShared()在循环中每次尝试获取锁时,都是通过tryAcquireShared()来进行尝试的。下面看看“获取共享锁”的详细流程。
lock()
1 | public void lock() { |
acquireShared()
Sync继承于AQS,acquireShared()定义在AQS中。源码如下:
1 | public final void acquireShared(int arg) { |
acquireShared()首先会通过tryAcquireShared()来尝试获取锁。
尝试成功的话,则不再做任何动作(因为已经成功获取到锁了)。
尝试失败的话,则通过doAcquireShared()来获取锁。doAcquireShared()会获取到锁了才返回。
tryAcquireShared()
1 | protected final int tryAcquireShared(int unused) { |
tryAcquireShared()的作用是尝试获取“共享锁”。
如果在尝试获取锁时,“不需要阻塞等待”并且“读取锁的共享计数小于MAX_COUNT”,则直接通过CAS函数更新“读取锁的共享计数”,以及将“当前线程获取读取锁的次数+1”。
否则,通过fullTryAcquireShared()获取读取锁。
fullTryAcquireShared()
1 | final int fullTryAcquireShared(Thread current) { |
fullTryAcquireShared()会根据“是否需要阻塞等待”,“读取锁的共享计数是否超过限制”等等进行处理。如果不需要阻塞等待,并且锁的共享计数没有超过限制,则通过CAS尝试获取锁,并返回1。
doAcquireShared()
1 | private void doAcquireShared(int arg) { |
doAcquireShared()的作用是获取共享锁。
它会首先创建线程对应的同步队列的结点,然后将该结点添加到同步队列中。同步队列是管理获取锁的等待线程的队列。
如果“当前线程”是同步队列的表头,则尝试获取共享锁;否则,则需要通过shouldParkAfterFailedAcquire()判断是否阻塞等待,需要的话,则通过parkAndCheckInterrupt()进行阻塞等待。
doAcquireShared()会通过for循环,不断的进行上面的操作;目的就是获取共享锁。需要注意的是:doAcquireShared()在每一次尝试获取锁时,是通过tryAcquireShared()来执行的!
释放共享锁
释放共享锁的思想,是先通过tryReleaseShared()尝试释放共享锁。尝试成功的话,则通过doReleaseShared()唤醒“其他等待获取共享锁的线程”,并返回true;否则的话,返回flase。
unlock()
1 | public void unlock() { |
该函数实际上调用releaseShared(1)释放共享锁。
releaseShared()
1 | public final boolean releaseShared(int arg) { |
releaseShared()的目的是让当前线程释放它所持有的共享锁。
它首先会通过tryReleaseShared()去尝试释放共享锁。尝试成功,则直接返回;尝试失败,则通过doReleaseShared()去释放共享锁。
tryReleaseShared()
1 | protected final boolean tryReleaseShared(int unused) { |
tryReleaseShared()的作用是尝试释放共享锁。
doReleaseShared()
1 | private void doReleaseShared() { |
doReleaseShared()会释放“共享锁”。它会从前往后的遍历同步队列,依次“唤醒”然后“执行”队列中每个节点对应的线程;最终的目的是让这些线程释放它们所持有的锁。
公平/非公平共享锁
和互斥锁ReentrantLock一样,ReadLock也分为公平锁和非公平锁。
公平锁和非公平锁的区别,体现在判断是否需要阻塞的函数readerShouldBlock()是不同的。
公平锁的readerShouldBlock()的源码如下:
1 | final boolean readerShouldBlock() { |
在公平共享锁中,如果在当前线程的前面有其他线程在等待获取共享锁,则返回true;否则,返回false。
非公平锁的readerShouldBlock()的源码如下:
1 | final boolean readerShouldBlock() { |
在非公平共享锁中,它会无视当前线程的前面是否有其他线程在等待获取共享锁。只要该非公平共享锁对应的线程不为null,则返回true。