前言
很多时候,我们使用重入锁,是为了锁住共有资源保证每次只有一个线程进行修改,但是我们很多时候只是去读这些资源,如果每次都用reentrantLock锁的话,反而会降低吞吐量。
锁的细粒度可以增加吞吐量,读写锁通过16位低位去写锁,高16位去读锁还是挺有意思的。
写锁
在进行查看源码的时候先进行思考一下,写锁 和ReentrantLock 感觉差不多的底层实现应该是差不多的。也分公平锁,和非公平锁。
下面看看写锁的源码:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0) 说明没有独占锁,共享锁此时被占有,然后加入到同步队列尾部,这里猜想是 让持有读资源的线程先执行完,这里会导致写锁重入读锁时,进入到死锁当中
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//超过了记录的65535位
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire 重入增加state
setState(c + acquires);
return true;
}
// writerShouldBlock这个代码挺关键的,这个方法是公平和非公平的关键所在,如果是非公平锁,直接返回false,公平锁就去调用一下hasQueuedPredecessors() 这个方法去判断同步队列中有没有等待的线程,有一个模版模式的体现。讲到这里基本上公平锁和非公平锁就讲到了。
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
exclusiveCount(acquires)
这个方法还是很有意思的,官方的翻译是返回count中表示的独占保持的数量,感觉这里还要画一下图进行解释一下了。写锁拿的是前16位低位锁。读锁拿的是16位 高位锁,还是有意思的这样设计,因为想要读写锁共用,势必要用到state,所以通过高位和地位进行区分,分别获取了哪些锁
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
如果state为1 两者之间进行按位与运算的话得到的结果为 1
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static int exclusiveCount(int c) {
return c & EXCLUSIVE_MASK;
}
tryRelease(int releases)
这个方法应该是和可重入锁的释放接口,差不多,都是state释放完后,返回true,只不过这个是16位低位释放,应该有这个点,下面看代码分析
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
// 直接拿低位16位做与运算,如果都是0,肯定说明释放完了。
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
注意点:这里的释放和可重入锁的释放还不一样,这里是只释放一次,写下面的那种代码就会出现问题,当时我看到这里还挺奇怪的,为什么要这样写。就会出现线程一直挂在同步队列上面
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
// reentrantReadWriteLock.readLock().lock();
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
for (int i = 1; i <= 3; i++) {
writeLock.lock();
}
writeLock.unlock();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
writeLock.lock();
System.out.println();
writeLock.unlock();
}).start();
读锁
为什么要设计读锁呢?因为业务中其实大部分的都是读的情况,写的情况少。之前已经提到了读锁的state是高16位开始的,那么65536 就是最基本的增加计量单位了,对吧。看源码实现,还有之前写锁分析的时候,有段代码是如果有state,但是有没有线程占领写资源的时候,有个写线程进入的话,是会加入到同步队列中等待读锁执行完的。
tryAcquire中有个这样的判断
if (w == 0 || current != getExclusiveOwnerThread())
return false;
通过以下代码可以复现:
public static void main(String[] args) throws Exception {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
reentrantReadWriteLock.readLock().lock();
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
for (int i = 1; i <= 3; i++) {
writeLock.lock();
}
writeLock.unlock();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
writeLock.lock();
System.out.println();
writeLock.unlock();
}).start();
}
下面开始看读锁的源码
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 如果独占锁持有的不为0,并且持有独占锁的线程不是当前线程,读锁加入到同步队列中去,
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 共享锁持有的数量
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 如果是第一次读,就加一,如果是同一个第一个线程则++
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
// 这个cacheHoldCounter 记录了上一个线程获取锁的次数,以及线程id
HoldCounter rh = cachedHoldCounter;
// 如果是null的话获取一个初始值,如果不是同一个线程了,记录新的线程
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
//
return fullTryAcquireShared(current);
}
非公平共享锁的处理方法
重点讲解一下这里的方法
这只是一种启发式地避免写锁无限等待的做法,它在遇到同步队列的head后继为写锁节点时,会让readerShouldBlock返回true代表新来的读锁(new reader)需要阻塞等待这个head后继。但只是一定概率下能起到作用,如果同步队列的head后继是一个读锁,之后才是写锁的话,readerShouldBlock就肯定会返回false了
final boolean readerShouldBlock() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
公平共享锁的处理方法
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
ThreadLocalHoldCounter重写了ThreadLocal的 initialValue 方法,使得HoldCounter 进行了初始化,所以第一次获取的时候是有值的
//readHolds 这个记录了每个线程持有锁的次数
private transient ThreadLocalHoldCounter readHolds;
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
如果 if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) 这个判断为false,则会进行下面的方法
fullTryAcquireShared(Thread current) 这个方法很关键
总结如下:
- 判断是否有写锁持锁
- 判断 onSyncQueue 有无等待者之类
- 高位不能超过 65535
- 进行 CAS 操作
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
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;
}
}
}
读锁的释放锁资源
先思考一下加锁的过程,写锁是对state进行一个递加来表示重入,但是读锁不行,因为我可能是很多的线程,所以读锁引入了HoldCounter 这个对象来存储线程重入的次数,多个线程用的threadlocal,ThreadLocalHoldCounter进行存储
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
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;
}
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;
}
}
读锁套写锁会写锁
当执行下面这段代码的时候就会导致死锁
public static void main(String[] args) throws Exception {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
readLock.lock();
System.out.println("写锁重入开始");
writeLock.lock();
}
为什么呢?首先读锁获取到锁资源,然后state此时应该为65536
然后写锁去获取资源的时候会进入下面这段代码
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
return false;
然后就会返回false,进入到acquirdQueued方法,然后挂起。读锁就永远不能进行unlock了。
但是先执行写锁lock,就不会导致死锁了,因为此时已经拿到了独占资源了。
锁降级
关于jdk对于锁降级的定义如下:
锁降级:
重入还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不可能的。其实就是线程先获取到了写锁,还没有释放写锁的资源的时候,再去获取读锁,是可以的。具体的代码在tryAcquireShared(int unused) 方法和fullTryAcquireShared(Thread current)方法中都有体现
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
获取读锁的时候会去判断是不是有写锁拿到独占资源了,如果有独占资源是不是当前线程的,如果不是当前线程的则去加入到同步队列中,如果是当前的线程则直接可以获取到读锁,这样做的好处就是避免了拿到独占锁后,释放才能获取读锁,因为释放掉才能获取读锁,又要和别的线程进行竞争,读到的数据并不是最开始修改的值。