概述
ReentrantReadWriteLock
读写锁是使用AQS的集大成者,用了独占模式和共享模式。本文和大家一起理解下ReentrantReadWriteLock
读写锁的实现原理。在这之前建议大家阅读下下面3篇关联文章:
通俗易懂读写锁ReentrantReadWriteLock的使用
原理概述
上图是ReentrantReadWriteLock
读写锁的类结构图:
- 实现了
ReadWriteLock
接口,该接口提供了获取读锁和写锁的API。 ReentrantReadWriteLock
读写锁内部的成员变量readLock是读锁,指向内部类ReadLock。ReentrantReadWriteLock
读写锁内部的成员变量writeLock是写锁,指向内部类WriteLock。ReentrantReadWriteLock
读写锁内部的成员变量sync是继承AQS的同步器,他有两个子类FairSync
公平同步器和NoFairSync
非公平同步器,读写锁内部也有一个sync,他们使用的是同一个sync。
读写锁用的同一个sync同步器,那么他们共享同一个state, 这样不会混淆吗?
不会,ReentrantReadWriteLock
读写锁使用了AQS中state值得低16位表示写锁得计数,用高16位表示读锁得计数,这样就可以使用同一个AQS同时管理读锁和写锁。
- ReentrantReadWriteLock类重要成员变量
// 读锁 private final ReentrantReadWriteLock.ReadLock readerLock; // 写锁 private final ReentrantReadWriteLock.WriteLock writerLock; // 同步器 final Sync sync;
- ReentrantReadWriteLock构造方法
//默认是非公平锁,可以指定参数创建公平锁 public ReentrantReadWriteLock(boolean fair) { // true 为公平锁 sync = fair ? new FairSync() : new NonfairSync(); // 这两个 lock 共享同一个 sync 实例,都是由 ReentrantReadWriteLock 的 sync 提供同步实现 readerLock = new ReadLock(this); writerLock = new WriteLock(this); }
- Sync类重要成员变量
// 用来移位 static final int SHARED_SHIFT = 16; // 高16位的1 static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 65535,16个1,代表写锁的最大重入次数 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 低16位掩码:0b 1111 1111 1111 1111,用来获取写锁重入的次数 static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 获取读写锁的读锁分配的总次数 static int sharedCount(int c) { return c >>> SHARED_SHIFT; } // 写锁(独占)锁的重入次数 static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
加锁原理
图解过程
设计一个加锁场景,t1线程加写锁,t2线程加读锁,我们看下它们整个加锁得流程。
- t1 加写锁
w.lock()
成功,占了 state 的低 16 位。
- 这里得state分为两部分
0_1
,0表示高16位的值,1表示低16位的值。 - AQS当前占用线程
exclusiveOwnerThread
属性指向t1线程。
- t2线程执行加读锁
r.lock()
,尝试获取锁,发现已经被写锁占据了,加锁失败。
- t2线程被封装成一个共享模式Node.SHARED的节点,加入到AQS的队列中。
- 在阻塞前,t2线程发现自己是队列中的老二,会尝试再次获取读锁,因为t1没有释放,它会失败,然后它会把队列的前驱节点的状态改为-1,然后阻塞自身,也就是t2线程。
- 上面中黄色三角形就是等待状态的值,前驱节点变成-1
- 上面中的灰色表示节点所在的线程阻塞了
- 后面如过有其他线程如t3,t4加读锁或者写锁,由于t1线程没有释放锁,会变成下面的状态。
上面是整个解锁的流程,下面深入源码验证这个流程。
源码解析
- 写锁加锁源码
WriteLock类的lock()方法是加写锁的入口方法。
static final class NonfairSync extends Sync { // ... 省略无关代码 // 外部类 WriteLock 方法, 方便阅读, 放在此处 public void lock() { sync.acquire(1); } // AQS 继承过来的方法, 方便阅读, 放在此处 public final void acquire(int arg) { if ( // 尝试获得写锁失败 !tryAcquire(arg) && // 将当前线程关联到一个 Node 对象上, 模式为独占模式 // 进入 AQS 队列阻塞 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) ) { selfInterrupt(); } } protected final boolean tryAcquire(int acquires) { // 获取当前线程 Thread current = Thread.currentThread(); //获得锁的状态 int c = getState(); // 获得低 16 位, 代表写锁的 state 计数 int w = exclusiveCount(c); // c不等于0表示加了读锁或者写锁 if (c != 0) { if ( // c != 0 and w == 0 表示有读锁返回错误,读锁不支持锁升级, 或者 w == 0 || // w != 0 说明有写锁,写锁的拥有者不是自己,获取失败 current != getExclusiveOwnerThread() ) { // 获得锁失败 return false; } // 写锁计数超过低 16 位最大数量, 报异常 if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // 写锁重入, 获得锁成功,没有并发,所以不使用 CAS setState(c + acquires); return true; } if ( // c == 0,说明没有任何锁,判断写锁是否该阻塞,是 false 就尝试获取锁,失败返回 false writerShouldBlock() || // 尝试更改计数失败 !compareAndSetState(c, c + acquires) ) { // 获得锁失败 return false; } // 获得锁成功,设置锁的持有线程为当前线程 setExclusiveOwnerThread(current); return true; } // 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞 final boolean writerShouldBlock() { return false; } // 公平锁会检查 AQS 队列中是否有前驱节点, 没有(false)才去竞争 final boolean writerShouldBlock() { return hasQueuedPredecessors(); } }
tryAcquire()
方法是模板方法,由子类自定义实现获取锁的逻辑。- 线程如果获取写锁失败的话,通过
acquireQueued()
方法封装成独占Node加入到AQS队列中。
2.读锁加锁源码
ReadLock
类的lock()
方法是加读锁的入口方法,调用tryAcquireShared()
方法尝试获取读锁,返回负数,失败,加入到队列中。
// 加读锁的方法入口 public void lock() { sync.acquireShared(1); } public final void acquireShared(int arg) { // tryAcquireShared 返回负数, 表示获取读锁失败,加入到队列中 if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }