ReentrantReadWriteLock简介
①读写锁在同一时刻可允许多个读线程访问,但在写线程访问时,所有的读线程和其他写线程均被阻塞。保证了写操作对读操作的可见性
②读写锁维护了一对锁,一个读锁和一个写锁,分离读写锁提升并发性能
③一般情况下,读写锁比排它锁有更好的吞吐量和并发性
ReentrantReadWriteLock特性
公平性:支持公平和非公平获取锁方式,非公平吞吐量优于公平
重入:支持重入,读锁获取读锁后可以再次获取读锁,写锁获取写锁后可再次获取写锁,同时也可以获取读锁
降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁
ReentrantReadWriteLock结构
从上图可以看出:
1.读写锁ReentrantReadWriteLock内部维护了一个ReadLock读锁嵌套类和一个WriteLock写锁嵌套类。
2.ReadLock与WriteLock使用的时同一个Sync实例。Sync通过ReentrantReadWriteLock构造参数来控制使用公平、非公平模式。
1.Sync继承了AQS
2.公平模式FairSync和非公平模式NonfairSync继承自Sync
读写锁实现类,从下图可以看出,ReadLock使用了共享模式,WriteLock使用了独占模式
AQS内部属性state:
对独占模式来说,通常0代表可获取锁,1 代表锁已被别人占用(重入例外),而共享模式下,每个线程都可以对state进行加减操作,也就是说,独占模式和共享模式对于state的操作完全不一样,那读写锁ReentrantReadWriteLock中state如何使用?
读写锁实现原理
读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态state,ReentrantLock中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态。整型变量按位切割使用,高16位表示读,低16位表示写。
上图得出:当前同步状态表示一个线程已经获取了写锁且重入了两次,同时也连续获取了两次读锁。
假设 state = S
写状态 = S & 0x0000FFFF
读状态 = S >>> 16(无符号补0右移16位)
写状态 + 1 = S + 1
读状态 + 1 = S + (1 << 16) = S + 0x00010000
推论:S不等于0时,当写状态(S & 0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。
源码分析
abstract static class Sync extends AbstractQueuedSynchronizer {
static final int SHARED_SHIFT = 16;
// 这个是读锁的原始累加值(也就是说每次获取读锁都是获取状态state,然后用state加它)是2^16
// 举个例子,假设现在state为1,那么现在来获取读锁就是1+SHARED_UNIT
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 读锁和写锁的最大数量,都是2^16 - 1
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 写锁的掩码,其实就是2^16 - 1,这个数的二进制很特殊,16位全是1
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 取c的高16位,代表读锁的获取次数(包括重入)
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 取c的低16位,代表写锁的重入次数,因为写锁是独占模式,只能由一个线程获取
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
// 这个嵌套类的实例用来记录每个线程持有的读锁数量(读锁重入)
static final class HoldCounter {
// 持有的读锁数
int count = 0;
// 线程id
final long tid = getThreadId(Thread.currentThread());
}
// ThreadLocal的子类
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
// 记录当前线程持有的读锁数量
private transient ThreadLocalHoldCounter readHolds;
// 用于缓存,避免从ThreadLocal的Map中查找,性能会好一些
// 记录最后一个获取读锁的线程的重入次数
private transient HoldCounter cachedHoldCounter;
// 第一个获取读锁的线程(并且其未释放读锁)以及它持有的读锁数量
// 这两个属性在读锁没有竞争情况下,很方便记录读锁重入次数
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
Sync() {
// 初始化readHolds这个ThreadLocal属性
readHolds = new ThreadLocalHoldCounter();
// 为了保证readHolds的内存可见性
setState(getState());
}
...
}
1.读锁获取
// ReadLock
public void lock() {
sync.acquireShared(1);
}
// AQS
public final void acquireShared(int arg) {
// 调用Sync类的tryAcquireShared方法
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 && // 已经有线程获取写锁
getExclusiveOwnerThread() != current) // 获取写锁线程不是当前线程
return -1; // 获取读锁失败
// 如果持有写锁的是当前线程,则可以继续去获取读锁
// 读锁的获取次数
int r = sharedCount(c);
if (!readerShouldBlock() && // 读锁获取不会被阻塞
r < MAX_COUNT && // 是否溢出,超过读锁允许获取最大限制
compareAndSetState(c, c + SHARED_UNIT)) { // 读锁+1,因为是高16位,+SHARED_UNIT
if (r == 0) {
// r == 0说明此线程是第一个获取读锁,记录firstReader为当前线程及其持有的读锁数量1
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// firstReader重入,简单+1即可
firstReaderHoldCount++;
} else {
// 进入到这里,说明当前线程不是第一个获取读锁的,也不是第一个获取读锁线程的重入
// cachedHoldCounter用于缓存最后一个获取读锁的线程(排除第一个,因为如果是第一个线程,使用了firstReader,那么它就不需要占用cachedHoldCounter)
HoldCounter rh = cachedHoldCounter;
// 如果缓存为空(不存在读锁竞争),如果缓存的不是当前线程则设置缓存为当前线程的HoldCounter。
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
// 缓存的是当前线程,但是count=0,把当前rh记录到ThreadLocal的map中
readHolds.set(rh);
rh.count++;
}
return 1; // 读锁获取成功
}
return fullTryAcquireShared(current);
}
上面分析了if分支,那么什么时候回进入到fullTryAcquireShared方法?(暂不考虑MAX_COUNT溢出情况)
第一种情况:readerShouldBlock返回true,分2种情况
FairSync.readerShouldBlock中执行hasQueuedPredecessors(),即阻塞队列中有其它元素在等待锁。也就是说公平模式下,有人在排队,你新来的不能直接获取锁。
NonFairSync.readerShouldBlock中执行apparentlyFirstQueuedIsExclusive(),即判断阻塞队列中head的第一个后继节点是否是来获取写锁的,如果是的话,让这个写锁先来,避免写锁饥饿。
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
第二种情况:compareAndSetState(c, c + SHARED_UNIT) 这里CAS失败,说明存在竞争。可能是和另一个读锁获取竞争,当然也可能是和另一个写锁获取操作竞争。
// 因为CAS失败,如果就此返回,那么就要进入到阻塞队列了,但它满足了!readerShouldBlock(),也就是说本来可以不用到阻塞队列,所以该方法是增加cas成功获取读锁的概率
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
// 如果由线程获取了写锁,并且不是当前线程,则获取读锁失败
return -1;
} else if (readerShouldBlock()) {
// 进入到这里,说明了
// 1.exclusiveCount(c) == 0:写锁没有被占用
// 2.公平模式readerShouldBlock()为true,说明阻塞队列中有其他线程在等待,非公平模式readerShouldBlock()为true,说明head.next是来获取写锁的
// 既然是shuold block,为何会进入到这里?
// 为了处理读锁重入
if (firstReader == current) {
// firstReader线程重入读锁,则到达下面的cas
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
// 缓存为空或缓存的不是当前线程,则从当前线程ThreadLocal的Map中获取
rh = readHolds.get();
if (rh.count == 0) // 说明是readHolds.get()初始化的HoldCounter,读锁计数0,非重入,直接删除
readHolds.remove();
}
}
if (rh.count == 0) // 读锁计数0,非重入
return -1; // 获取读锁失败,进入到阻塞队列排队去
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
// cas成功,表明获取了读锁
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
2.读锁释放
// ReadLock
public void unlock() {
sync.releaseShared(1);
}
// Sync
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared(); // 这句代码其实唤醒获取写锁的线程
return true;
}
return false;
}
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
if (firstReaderHoldCount == 1)
// 如果等于1,那么这次解锁后就不再持有锁了,把firstReader置为 null,给后来的线程用
// 为什么不顺便设置firstReaderHoldCount = 0?因为没必要,其他线程使用的时候自己会设值
firstReader = null;
else
firstReaderHoldCount--;
} else {
// 判断cachedHoldCounter是否缓存的是当前线程,不是的话要到ThreadLocal中取
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
// 这一步将ThreadLocal remove掉,防止内存泄漏。因为已经不再持有读锁了
readHolds.remove(); // 重复remove不会异常
if (count <= 0)
// 就是那种,lock()一次,unlock()好几次的逗比
throw unmatchedUnlockException();
}
// count减1
--rh.count;
}
for (;;) {
int c = getState();
// nextc是state高16位减1后的值
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// 如果nextc == 0,那就是state全部32位都为 0,也就是读锁和写锁都空了
// 此时这里返回true的话,其实是帮助唤醒后继节点中的获取写锁的线程
return nextc == 0;
}
}
// 该方法分析见AQS源码分析三 https://blog.youkuaiyun.com/jiangtianjiao/article/details/103915459
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
读锁释放的过程还是比较简单的,主要就是将 hold count 减 1,如果减到 0 的话,还要将 ThreadLocal 中的 remove 掉。然后是在 for 循环中将 state 的高 16 位减 1,如果发现读锁和写锁都释放光了,那么唤醒后继的获取写锁的线程。
3.写锁获取
写锁是独占锁,如果有读锁被占用,写锁获取是要进入到阻塞队列中等待的。
// WriteLock
public void lock() {
sync.acquire(1);
}
// AQS
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
// 如果tryAcquire失败,那么进入到阻塞队列等待
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// 返回false存在的情况:
// c != 0 && w == 0: 写锁可用,但是有线程持有读锁(也可能是自己持有)
// c != 0 && w !=0 && current != getExclusiveOwnerThread(): 其它线程持有写锁
// 也就是说只要有读锁或写锁被占用,这次就不能获取到写锁
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 这里不需要CAS,能到这里的只能是写锁重入,不然在上面的if处current != getExclusiveOwnerThread()就会被拦截
setState(c + acquires);
return true;
}
// 如果写锁获取不需要block,那么进行CAS,成功就代表获取到了写锁
if (writerShouldBlock() || // 如果是非公平模式,writerShouldBlock()返回false,则直接cas抢锁,如果是公平模式,那么如果阻塞队列有线程等待的话,就乖乖去排队
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
4.写锁释放
// WriteLock
public void unlock() {
sync.release(1);
}
// AQS
public final boolean release(int arg) {
// 1.释放锁
if (tryRelease(arg)) {
// 2.如果独占锁释放完全,唤醒后继节点
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// 释放锁过程是线程安全的,因为写锁是独占锁,具有排他性
// 实现很简单,state减1就是了
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
// 如果 exclusiveCount(nextc) == 0,也就是说包括重入的,所有的写锁都释放了,
// 那么返回true,这样会进行唤醒后继节点的操作。
return free;
}