实际上,在多线程并发环境下,读数据不会对数据造成修改,因此不需要加锁就可以。在ReentrantReadWriteLock类中,将读操作和写操作拆分为两把锁,分别是读锁:ReadLock和写锁:WriteLock。这种读写分离的操作,运行多线程同时读取数据,在写操作的时候,才是独占的。
我们先来看看ReentrantReadWriteLock的类图,了解其结构。这样我们看源码也能一目了然,印象也会深刻。
其实,这个类图可以看出,ReentrantReadWriteLock实现了ReadWriteLock和Serializable接口。其内部有个Sync类继承自AQS,同时这个类需要实现公平锁和非公平锁,因此Sync有两个子类:FairSync和NonfairSync类。
这里说明一下,ReentrantReadWriteLock里面有两个内部类:ReadLock和WriteLock,是因为要实现ReadWriteLock的接口的。在ReadWriteLock接口中定义了ReadLock和WriteLock的方法用于返回读锁和写锁。
ReentrantReadWriteLock的实现和之前的ReentrantLock有点像。首先,我们来看看其构造函数。其空参数的构造函数默认是使用内部的非公平锁,还可以传入参数,指定创建公平或非公平锁,源码如下所示。
再扯远一点,在ReentrantReadWriteLock中有个方法:boolean isFair(),这个类似于Netty框架中判断一个Handler是inBound还是OutBound类型,也是这样判断的。源码如下:
下面进入正题,下面来看读锁的源码。读锁中最重要的两个方法就是lock和unlock方法。下面我们先来看看lock方法源码。
这个和之前ReentrantLock实现类似,由于读锁支持多线程同时访问,因此这里使用AQS中的共享锁。我们继续跟进去看看acquireShared()源码。
其中,tryAcquireShared()需要AQS的子类去实现,这里在Sync类中实现了。而doAcquireShared方法是在AQS中实现的。下面我们来看看tryAcquireShared源码。
这里主要分为以下几步:
1、首先获取State的值,然后调用exclusiveCount方法来判断写锁是否占用 ,当该方法返回值不为0,就说明写锁被占用。这里如果当前线程获取写锁,那么也是允许获取读锁的。
2、获取读锁的次数,也就是调用sharedCount方法进行计算。这里注意,写锁占用了State变量的低16位,读锁占用高16位。
3、这里readShouldBlock方法在Sync中是抽象方法,需要FairSync和NonFairSync去实现。
如果是公平锁,实现如下。原理是去查看AQS的阻塞队列,如果队列里面有挂起的线程,那么返回true,那么上面的tryAcquireShared函数直接进入最后fullTryAcquireShared去自旋获取锁。
如果是非公平锁,实现如下。其原理是判断队列中是否有元素在获取写锁。
4、继续回到tryAcquireShared函数,如果成功获取读锁,那么如果r=0,那就是第一次获取读锁,这里用firstReadHoldCount来记录同一个线程读的次数。HoldCount用于记录其他线程的读次数。
下面来看看unlock的源码。
进入releaseShared源码看看。
这里tryReleaseShared需要AQS的子类实现,这里在Sync中实现。该方法的主要作用是在for(;;)里面,用CAS操作设置state的值为nextc,nextc也就是减去一个读单位的值。如果nextc为0,说明当前没有线程占用读锁,那么返回true。然后releaseShared函数进入doReleaseShared,去唤醒其他因为写操作阻塞的线程(比如其他线程获取写锁,那么读操作和写操作不能同时进行,读操作线程会阻塞)。