文章目录
读写锁ReentrantReadWriteLokc原理
解决线程安全问题用ReentrantLock就可以了,但是ReentrantLock是独占锁,某时只有一个线程可以获取该锁,而实际中会有写少读多的场景,显然ReentrantLock满足不了这个需求,所以ReentrantReadWriteLock应运而生。ReentrantReadWriteLock采用读写分离的策略,允许多个线程可以同时获取锁。
一、类图结构
读写锁的内部维护了一个ReadLock和一个WriteLock,他们依赖Sync实现具体功能。而Sync继承自AQS,并且也提供了公平和非公平的实现。下面只介绍非公平的读写锁实现。我们知道AQS只维护了一个state状态,而ReentrantReadWriteLock则需要维护读状态和写状态,一个state怎么表示写和读两种状态呢?ReentrantReadWriteLock巧妙地使用state的高16位表示读状态,也就是获取到读锁的次数;使用低16位表示获取写锁的线程可重入次数。
其中firstReader用来记录第一个获取读锁的线程,firstReaderHoldCount则记录第一个获取到读锁的线程获取读锁的可重入次数。cachedHoldCounter用来记录最后一个获取读锁的线程获取读锁的可重入次数。
readHolds
是ThreadLocal变量,用来存放除去第一个获取读锁线程外的其他线程获取读锁的可重入次数。ThreadLocalHoldCounter继承了ThreadLocal,因而initialValue方法返回一个HoldCounter对象。
二、写锁的获取与释放
在ReentrantReadWriteLock中写锁使用WriteLock实现。
1、void lock()
写锁是一个独占锁,某时只有一个线程可以获取该锁。如果当前没有线程获取到读锁和写锁,则当前线程可以获取写锁然后返回。如果当前已经有线程获取到读锁和写锁,则当前请求写锁的线程会被阻塞挂起。另外,写锁是可重入锁,如果当前线程已经获取了该锁,再次获取只是简单地把可重入次数加1然后直接返回。
如上代码所示,在lock内部调用了AQS的acquire方法,其中tryAcquire是ReentrantReadWriteLock内部的sync类重写的,代码如下。
在代码(1)中,如果当前AQS状态值不为0,说明当前已经有线程获取了读锁或者写锁。在代码(2)中,如果w == 0说明状态值的低16位是0,而AQS状态值不为0,说明高16位状态不为0,这暗示已经有线程获取了读锁,直接返回false。而如果w != 0则说明当前已经有线程获取了写锁,再看当前线程是不是该锁的拥有者,如果不是则返回false。
执行到代码(3)说明当前线程之前已经获取到了该锁,所以判断该线程的可重入次数是不是超过了最大值,是则抛出异常,否则执行代码(4)增加当前线程的可重入次数,然后返回true。
如果AQS的状态值等于0则说明目前没有线程获取到读锁和写锁,所以执行代码(5)。其中,对于writerShouldBlock方法,非公平锁的实现为
以上代码对于非公平锁来说总是返回false,则说明代码(5)抢占式执行CAS尝试获取写锁,获取成功则设置当前锁的持有者为当前线程并返回true,否者返回false。
公平锁的实现为
这里还是使用hasQueuedPredecessors来判断当前线程节点是否有前驱节点,如果有则当前线程放弃获取写锁的权限,直接返回false。
2、void lockInterruptibly()
类似lock方法,它的不同之处在于,他会对中断进行响应,也就是当其他线程调用该线程的interrupt方法中断了当前线程时,当前线程会抛出异常InterruptedException异常。
3、boolean tryLock()
尝试获取写锁,如果当前没有其他线程持有写锁或者读锁,则当前线程获取写锁会成功,然后返回true。如果当前已经有其他线程持有写锁或读锁,则该方法直接返回false,且当前线程并不会阻塞。如果当前线程已经持有了写锁则简单增加AQS的状态值后直接返回true。
如上代码与tryAcquire类似,不在赘述,不同在于使用的是非公平策略。
4、boolean tryLock(long timeout, TimeUnit unit)
与tryAcquire的不同之处在于,多了超时时间参数,如果尝试获取写锁失败则会把当前线程挂起指定时间,待超时时间到后当前线程被激活,如果还是没有获取到写锁则返回false。另外,该方法会对中断进行响应,也就是当其他线程调用了该线程的interrupt()方法中断了当前线程时,当前线程会抛出异常。
5、void unlock()
尝试释放锁,如果当前线程持有该锁,调用该方法会让线程对该线程持有的AQS状态值减1,如果减去1后当前状态值为0则当前线程会释放该锁,否则仅仅减1而已。如果当前线程没有持有该锁而调用了该方法则会抛出IllegalMonitorStateException异常,代码如下。
在如上代码中,tryRelease首先通过isHeldExclusively判断是否当前线程是改写锁的持有者,如果不是则抛出异常,否则执行代码(7),这说明当前线程持有写锁,持有写锁说明状态值高16位为0,所以这里nextc的值就是当前线程写锁的剩余可重入次数。代码(8)判断当前可重入次数是否为0,如果free为true则说明可重入次数为0,所以当前线程会释放写锁,将当前锁的持有者设置为null。如果free为false则简单地更新可重入次数。
三、读锁的获取与释放
ReentrantReadWriteLock中的读锁是使用ReaLock实现的。
1、void lock()
获取读锁,如果当前没有其他线程持有写锁,则当前线程可以获取读锁,AQS的状态值state的高16位的值会增加1,然后方法返回。否则如果其他一个线程持有写锁,则当前线程会被阻塞。
在如上代码中,读锁的locak方法调用了AQS的acquireShared方法,在其内部调用ReentrantReadWriteLock中的sync重写的tryAcquireShared方法,代码如下。
如上代码首先获取了当前AQS的状态值,然后代码(2)查看是否有其他线程获取了写锁,如果是则直接返回-1,而后调用AQS的doAcquireShared方法把当前线程放入AQS阻塞队列。
如果当前要获取写锁的线程已经持有了写锁,则也可以获取读锁。但是需要注意的是,当一个线程先获取了写锁,然后获取了读锁处理事情完毕后,要记得把读锁和写锁都释放掉,不能只释放写锁。
否则执行代码(3),得到获取到的读锁个数,到这里说明目前没有线程获取写锁,但是可能有线程持有读锁,然后执行代码(4)。其中非公平锁的readerShouldBlock实现代码如下。
如上代码的作用是,如果队列里面存在一个元素,则判断第一个元素是不是正在尝试获取写锁,如果不是,则当前线程判断当前获取读锁的线程是否达到了最大值。最后执行CAS操作将AQS的状态值高16位增加1。
代码(5)(6)记录第一个获取读锁的线程并统计该线程获取读锁的可重入次数。代码(7)使用cachedHoldCounter记录最后一个获取到读锁的线程和该线程获取读锁的可重入数,readHolds记录了当前线程获取读锁的可重入数。
如果readerShouldBlock返回true则说明有线程正在获取写锁,所以执行代码(8)。fullTryAcquireShared的代码与tryAcquireShared类似,他们的不同之处在于,前者通过循环自旋获取。
2、void lockInterruptibly()
类似于lock()方法,不同之处在于,该方法会对中断进行响应,也就是当其他线程调用了该线程的interrupt方法中断了当前线程时,当前线程会抛出InterruptedException异常。
3、boolean tryLock()
尝试获取读锁,如果当前没有其他线程持有写锁,则当前线程获取读锁会成功,然后返回true。如果当前已经有其他线程持有写锁则该方法直接返回false,但当前线程并不会被阻塞。如果当前线程已经持有了改读锁则简单增加AQS的状态值高16位后直接返回true。其代码类似tryLock的代码,这里不再赘述。
4、boolean tryLock(long timeout, TimeUnit unit)
与tryLock的不同之处在于,多了超时时间参数,如果尝试获取读锁失败则会把当前线程挂起指定时间,待超时时间到后当前线程被激活,如果此时还没有获取到读锁则返回false。另外,该方法对中断响应,也就是当其他线程调用了该线程的interrupt方法中断了当前线程时,当前线程会抛出InterruptedException异常。
5、void unlock()
如上代码所示,在无限循环里面,首先获取当前AQS状态值并将保存到变量c,然后变量c被减去一个读计数单位后使用CAS操作更新AQS状态值,如果更新成功则查看当前AQS状态值是否为0,为0则说明当前已经没有读线程占用读锁,则tryReleaseShared返回true,然后会调用doReleaseSHared方法释放一个由于获取写锁而被阻塞的线程,如果当前AQS状态值不为0,则说明当前还有其他线程持有了读锁,所有tryReleaseShared返回false。如果tryReleaseShard中的CAS更新AQS状态值失败,则自旋重试直到成功。
四、总结
本节介绍了读写锁ReentrantReadWriteLock的原理,它的底层是使用AQS实现的。ReentrantReadWriteLock巧妙地使用AQS的状态值的高16位表示获取到读锁的个数,低16位表示获取写锁的线程的可重入次数,并通过CAS对其进行操作实现了读写分离,这在读多写少的场景下比较适用。