基于AQS实现的ReentrantReadWriteLock源代码深入分析

📅 发布时间:2026/7/28 11:14:07
基于AQS实现的ReentrantReadWriteLock源代码深入分析 在ReentrantLock的独占模式下,当需要在一个读写高度竞争场景中使用它的lock.lock()时,你会发现这是独占锁,它会让大量其他读线程和写线程都进入CLH阻塞队列中等待,如果在“读多写少”的场景中,ReentrantLock的这种独占锁方式显然会降低并发性能,因此ReentrantReadWriteLock就是为了解决这种“读多写少”的场景:读操作可以请求读锁(共享锁),意味着多个读线程可以并发读,写操作可以请求独占锁,其他写线程和读线程都需要等待。这里首先通过Doug Lea在源码注释给出的demo作为对ReentrantReadWriteLock一般用法说明:classCachedData{// 读线程需要对data读取,写线程可以对data进行更新,那么data显然是线程不安全的Objectdata;volatilebooleancacheValid;// 判断data是否已经被缓存// 1、创建一个读写锁finalReentrantReadWriteLockrwl=newReentrantReadWriteLock();//该方式的功能就是对已经缓存的数据进行预处理voidprocessCachedData(){// 2、由于一开始,我们乐观认为data可能已经被缓存,因此我们不急着申请独占锁,而是申请并发的共享读锁rwl.readLock().lock();// 3、结果发现:我们太乐观了,原来data没缓存,因此还不能读取数据(如果非得去读,只能读到旧数据)if(!cacheValid){// Must release read lock before acquiring write lock// 4、既然乐观错估了情况,那么只能用上“强大的独占锁”,保证自己能独占的实施“写操作、更新操作、删除操作”:因为读写锁是互斥的,需先释放读锁,然后在升级为写锁。// 这就是所谓的“锁升级”rwl.readLock().unlock();rwl.writeLock().lock();// 虽然说升级,但这里是在请求独占锁,同一时刻可能有其他线程已经拿到独占锁,因此这里可以会阻塞在AQS里面的CLH阻塞队列try{// Recheck state because another thread might have// acquired write lock and changed state before we did.// 5、由于我们获取写锁的时机也许比其他线程晚一步拿到,因此在这里拿到独占锁后,还得重新检查是否data已经提前被其他线程更新过if(!cacheValid){data=...// 例如从数据库重新读取最新的data,然后将cacheValid标记为true,表示data已经更新(或已经更新缓存)cacheValid=true;}// 6、由于我们还得使用更新后的data,因此可以申请读锁// Downgrade by acquiring read lock before releasing write lockrwl.readLock().lock();}finally{// 7、显然data已经被我“独占式”地更新过,可以释放写锁了rwl.writeLock().unlock();// Unlock write, still hold read}}try{// 8、在第6点获得共享的读锁后,在这里可以使用已经缓存的新datause(data);}finally{// 9、释放读锁rwl.readLock().unlock();}}}关于读锁、写锁、读线程、写线程的一些说明rwl.readLock().lock()`:很明显,线程在请求读锁,此锁是AQS的共享模式rwl.writeLock().lock():很明显,线程在请求写锁,此锁是AQS的独占模式严格来说:对于ReentrantReadWriteLock这种读写锁的设计,请求读锁的线程直接称为“读线程”可以吗,例如,请求写锁的线程,由于已经获取独占锁的线程可以去请求读锁,那么这个线程是应该称为“写线程”?“读线程”?还是“写、读线程”?还是“读写线程”呢? 这么看来,请求到读锁的线程似乎也不适合称为读线程?成功请求到写锁的线程也不适合称为写线程。也许换一个角度可以更容易区分:在用户代码层面的角度出发:1、用户设计此方法具有明显“读取”数据的逻辑时,该方法首先使用``rwl.readLock().lock(),过程中不管是否需要再次请求写锁,都可以把执行用户方法的线程称为“读线程”2、用户设计此方法具有明显“更新、删除、改、插入”数据的逻辑时,该方法首先使用``rwl.writeLock().lock(),过程中不管是否需要再次请求写锁、还是读锁,都可以把执行用户方法的线程称为“写线程”但是从ReentrantReadWriteLock内部设计来看,可能按以下思路去想会更清晰:1、仅在ReentrantReadWriteLock内部调用tryAcquire方法来看,调用tryAcquire方法的线程可以看成是写线程,因为它是从rwl.writeLock().lock()来的2、仅在ReentrantReadWriteLock内部调用tryAcquireShared方法来看,调用tryAcquireShared方法的线程可以看成是读线程,因为它是从rwl.readLock().lock()来的如果不按"用户代码"或者不按ReentrantReadWriteLock内部设计的角度,那么也可以按以下说明进行统一化描述:“请求读锁的线程”、“持有读锁的线程”“请求写锁的线程”、“持有写锁的线程”在持有写锁的情况下,请求读锁的线程。不存在“持有读锁的情况下,还能同时持有写锁的线程”的情形! (否则此读锁已不是共享锁,而是变相成为了独占锁)或者说不满足“读写互斥设计”关于同步状态值的设计在ReentrantLock中,如何记录线程已经获得锁资源的次数呢?回顾代码设计如下:finalThreadcurrent=Thread.currentThread();intc=getState();if(c==0){// 只有state为0时,其他线程才有机会争抢独占锁if(compareAndSetState(0,acquires)){setExclusiveOwnerThread(current);returntrue;}}elseif(current==getExclusiveOwnerThread()){// 同一线程再次获取独占锁,同一线程重入锁intnextc=c+acquires;if(nextc0)// overflowthrownewError("Maximum lock count exceeded");setState(nextc);returntrue;}returnfalse;}ReentrantLock仅有一个state值,对于同一线程执行lock.lock()n次,那么state变为n,表示此线程获取独占锁n次或者说重入n次(当然也要求此线程要释放n次),既然是独占锁,那么就不会出现这种情况:有多个不同线程同时成功更改state,也即同一个时刻,仅能有个线程CAS更改成功。而在ReentrantReadWriteLock的同步状态值中,state的设计非常巧妙,使用高16位作为读锁重入的计数,低16位作为线程写锁重入的计数对于一个int值通过位运算实现不同场景下的计数,在Doug Lea的并发设计里面一个高级的手段:例如在ConcurrentHashMap里面的resizeStamp也采用这种移位计算状态的策略,还有ConcurrentHashMap的TreeBin读写锁设计也是采用位运算策略。高16位作为读锁重入的计数有两种情况:(1)此高16位的数值可表示同一读线程的请求的读锁总数(总重入数)(2)此高16位的数值可表示多个读线程同时请求到读锁总数低16位作为写锁计数的情况仅有一种:因为写锁是独占锁,因此低16位一定是表示同一写线程请求的总独占锁数量(总重入数)以下是读写锁的同步计数移位计算的设计SHARED_SHIFT = 16 (1)读锁设计 static final int SHARED_UNIT = (1 SHARED_SHIFT); 用于获取state高16的读锁总数 0000 0000 0000 0000 0000 0000 0000 0001 // 1右移16位,对应的就是SHARED_UNIT 0000 0000 0000 0001 0000 0000 0000 0000 如何获取读锁总数? 0000 0000 0000 0110 0000 0000 0000 0010;某一时刻state的值 对其左移16位: 0000 0000 0000 0000 0000 0000 0000 0110 因此可以快速得出读锁数量:6个读锁(当然也同时存在某个线程重入2次的写锁) 以上就是sharedCount的设计 static int sharedCount(int c) { return c SHARED_SHIFT; } 如何实现读锁加1呢?如果直接state+1,那么这个加1是加在低位上,显然不合理,其实也很简单: state+(1SHARED_SHIFT),这就实现在在高16位的读锁加1 (2) 写锁设计 static final int EXCLUSIVE_MASK = (1 SHARED_SHIFT) - 1; 用于获取state低16位的写线程数量 任意的state值和这个独占锁掩码相与后即可得到state的低16位置,例如下面 0000 0000 0000 0110 0000 0000 0000 0010 ;;某一时刻state的值 0000 0000 0000 0000 1111 1111 1111 1111 ;独占锁掩码 两者取与后,得到写线程数量: 0000 0000 0000 0000 0000 0000 0000 0010 因此可以快速得出写锁数量:2个写锁(当然也同时存在6个读锁) 以上exclusiveCount的设计 static int exclusiveCount(int c) { return c EXCLUSIVE_MASK; } 写锁如何加1? state+(1 EXCLUSIVE_MASK),显然state+1就是在state的低16位上做累加,计算公式合理 写锁如何加n? state+(n EXCLUSIVE_MASK),显然state+n就是在state的低16位上做累加,计算公式合理 (3)读锁最大可重入数量为65535,当然写锁也是 static final int MAX_COUNT = (1 SHARED_SHIFT) - 1 0000 0000 0000 0001 0000 0000 0000 0000 减1 也即: 0000 0000 0000 0000 1111 1111 1111 1111写锁实现// 1、创建一个读写锁finalReentrantReadWriteLockrwl=newReentrantReadWriteLock();// 2、请求写锁rwl.writeLock().lock();原来写锁的获取并不像ReentrantLock使用lock.lock()的方式,而是通过writeLock().lock()获取,从方法名就知道这样设计为了方便使用者知道当前使用什么类型的锁。首先看其构造器:/** * Creates a new {@code ReentrantReadWriteLock} with * default (nonfair) ordering properties. */publicReentrantReadWriteLock(){// 默认构造器使用的是非公平模式this(false);}/** * Creates a new {@code ReentrantReadWriteLock} with * the given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */publicReentrantReadWriteLock(booleanfair){sync=fair?newFairSync():newNonfairSync();readerLock=newReadLock(this);// new时就已经实例化一个readerLock 对象和 writerLock 对象writerLock=newWriteLock(this);}// Sync当然是ReentrantReadWriteLock内部核心实现类,实现了AQS的tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared的关键逻辑,以及其他辅助方法abstractstaticclassSyncextendsAbstractQueuedSynchronizerstaticfinalclassNonfairSyncextendsSyncstaticfinalclassFairSyncextendsSync// WriteLock和 ReadLock 都是ReentrantReadWriteLock内部类publicReentrantReadWriteLock.WriteLockwriteLock(){returnwriterLock;}publicReentrantReadWriteLock.ReadLockreadLock(){returnreaderLock;}publicstaticclassReadLockimplementsLock,java.io.SerializablepublicstaticclassWriteLockimplementsLock,java.io