是什么
读写锁
读写锁什么效果
- 读读并行
- 读写串行
- 写写串行
- 所以只要有写的就会串行
- 关于重入锁,读读可以重入
- 关于重入锁,写写可以重入
- 关于重入锁,写读可以重入
- 关于重入锁,读写不可以重入,这个是为什么,因为假如读写可以重入,可能会造成死锁。
底层怎么区分读锁和写锁,是有两个锁嘛
public ReentrantReadWriteLock(boolean fair) {
//默认 非公平锁
sync = fair ? new FairSync() : new NonfairSync();
//ReadLock 读锁
readerLock = new ReadLock(this);
//WriteLock 写锁
writerLock = new WriteLock(this);
}
- 实体类分为读锁和写锁
- 所以读写锁是两个锁嘛?
写锁的lock方法
调用sync的acquire,sync是对象ReentrantReadWriteLock的属性
public void lock() {
sync.acquire(1);
}
acquire
来到 aqs的acquire,发现跟ReentrantLock一样,但是里面的实现不一样了
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire
来到 ReentrantReadWriteLock内部类Sync的tryAcquire来尝试获取锁
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//跟 ReentrantLock 一样 state 表示锁标识,0标识 未被加锁,大于0表示有被加锁
//不一样的是 state 高16位标识读锁,低16位标识写锁
int c = getState();
//1.exclusiveCount 里面的逻辑其实就是取 c 高位的值,具体做法如下
//2.c & ((1 << SHARED_SHIFT) - 1)
//3.(1 << 16) - 1
// 0000 0000 0000 0000 0000 0000 0000 0001
//<< 0000 0000 0000 0001 0000 0000 0000 0000
//-1 0000 0000 0000 0000 1111 1111 1111 1111
//4.c & 0000 0000 0000 0000 1111 1111 1111 1111
// **** **** **** **** **** **** **** ****
//& 0000 0000 0000 0000 1111 1111 1111 1111
// 0000 0000 0000 0000 **** **** **** ****
//5.通过上面的做法就可以把c低位取出,也就是写锁的标志
int w = exclusiveCount(c);
//如果 c 不为零 说明有被加锁
if (c != 0) {
//1.如果 w等于零 说明没有写锁,没有写锁,但是被加锁了
//说明有读锁,有读锁,立刻返回false,加锁失败,说明读写不能重入
//2.如果w不等于零,说明有写锁,如果有写锁但是获取锁的线程等于当前线程
//就可以重入,说明写写可以重入
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//重入锁,重入需要往低位加 acquires 判断是否大于十六位
//如果大于十六位需要抛出异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//设置 state
setState(c + acquires);
return true;
}
//1.如果 c = 0的情况
//2.writerShouldBlock 如果是非公平锁直接返回 false,如果是公平锁
//就会去判断是否需要排队
//3.如果 writerShouldBlock 返回 false 就通过 cas
//去获取锁,获取失败,返回false
//4.获取成功,设置当前线程为持有锁的线程
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
tryAcquire 总结
- 1.
tryAcquire是要尝试去获取锁 - 2.锁标志
state跟ReentrantLock类似0标识未被上锁,不一样的是高16位表示读锁,低十六位表示写锁,所以读锁跟写锁用的是同一个字段标识,但是高位地位区分 - 3.
tryAcquire会先去判断c是否等于零,也就是是否被加锁了,如果被加锁了,就判断是否是被读锁加锁了,读锁加锁之后,写锁不能重入,如果是写锁加锁了,写锁可以重入,可以重入之后还会判断,加锁之后state的低位是否超过十六位,超过报异常,如果可以重入,就往c加一 - 4.如果c等于零,也就是还未被加锁,如果是公平公锁会先去判断是否需要排队,如果是非公平锁就没有排队的判断,不需要排队,就通过
cas去修改c,cas成功就会设置持有锁的线程为当前线程,加锁成功。
acquireQueued addWaiter
acquireQueued 跟addWaiter的实现都跟ReentrantLock一样,加锁失败,把该节点放到队列排队等待被park
读锁的lock方法
调用sync的acquireShared去获取共享锁,sync跟写锁的sync对象是同一个对象,所以其实读写锁是同一把锁,只是加锁和解锁的实现不太一样。
public void lock() {
sync.acquireShared(1);
}
acquireShared
aqs的acquireShared
public final void acquireShared(int arg) {
if (
(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//1.exclusiveCount 获取排他锁也就是写锁标志
//2.如果写锁标志不为零,说明有写锁
//2.getExclusiveOwnerThread() != current 有写锁,而且当前线程不等于持有锁的线程
//返回-1 就是加锁失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//sharedCount 是将c移动16位,即是获取高16位
//高16位表示读锁的标志
int r = sharedCount(c);
//1.readerShouldBlock 判断读锁是否需要阻塞
//2.r < MAX_COUNT 判断读锁加锁次数是否大于最大值
//3.如果不需要阻塞,而且读锁加锁次数不大于加锁次数
//就通过cas设置c大小,加锁,注意是往c高位加一
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//r等于o说明目前没有读锁
if (r == 0) {
//设置第一个读锁的线程和读锁当前线程重入次数1
firstReader = current;
firstReaderHoldCount = 1;
//如果有读锁,而且当前第一个读锁线程等于当前读锁线程
//就往当前读锁线程重入次数加一
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
//1.cachedHoldCounter 缓存最近一次读锁
//2.如果缓存读锁为空或者当前线程非最近一次读锁的,就从 readHolds 重新获取
//3.往 HoldCounter 加一
//4.HoldCounter 是线程本地变量,存有线程的读锁重入次数
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
//返回加锁成功
return 1;
}
//如果需要排队或者cas 失败,就进入等待循环获取锁
return fullTryAcquireShared(current);
}
tryAcquireShared 总结
- 1.
state高16位存的是读锁的标志 - 2.会先判断是否加了写锁,而且持有锁的线程是否该线程,如果是就可以重入
- 3.如果写锁可以重入或者是被读锁加锁了,也可以在加读锁
- 4.读锁加锁首先会往
state的高16位加一 - 5.
HoldCounter放到线程本地变量,里面的属性有count也就是某个线程加锁次数 还有tid,表示所属线程的id
写锁的unLock方法
其实写锁的unLock 方法跟 ReentrantLock的解锁方法一样的
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
读锁的 unLock 方法
releaseShared 释放共享锁
public void unlock() {
sync.releaseShared(1);
}
releaseShared
aqs的 releaseShared
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
tryReleaseShared(arg)
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//如果第一个获取读锁的是当前线程就分为两种情况
//一种 firstReaderHoldCount == 1 没有重入,直接将 firstReader = null
//一种 firstReaderHoldCount != 1 有重入 firstReader -1
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
//如果不是第一个获取锁的线程,那线程锁记录标志到 HoldCounter
//获取到对应线程的 HoldCounter 然后对 HoldCounter 的value 减一
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
//以上两个 if 主要是对线程本地记录的获取锁次数的释放
//下面这个for 循环是对 state 的释放锁
//通过cas 修改 c 高16位的大小
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
tryReleaseShared 总结
- 1.释放读锁的时候有三个点
- 2.第一个点,会先去判断当前线程是否是
firstReader也就是第一个获取锁 如果是就firstReaderHoldCount减一 - 3.第二个点,如果当前线程不等于
firstReader,就会从 HoldCounter 的 value 减一 - 4.第三个点,自旋去
cas修改state的高16位减一
总结
- 1.
ReentrantReadWriteLock读写锁是一个锁,都是state控制的,只是高16位控制读锁 低16位控制写锁 - 2.写锁的加锁会先判断是否已经加锁了,如果已经加锁了,就会判断加的是否是写锁,如果写锁
可以重入,就往
state(低16位)加一,如果加的是读锁就不能重入加锁失败,如果没有加锁,就会就会通过 cas 去 修改 state 去尝试加锁,加锁失败就会将节点放到 aqs 队列中等待 unpark - 3.读锁加锁的时候会先判断有没有加写锁,如果有加写锁,又是当前线程,就可以重入,否则加锁失败。 如果没有加写锁,就可以加锁,加锁的过程有主要是数值加一,主要是 firstReaderHoldCount 和 HoldCounter 的 value 和 state (高16位)
- 4.读锁被 unpark 之后会去调用 doReleaseShared 去尝试 unpark 其他被park的读锁,也就是 共享锁会去尝试唤醒共享锁的线程。