/** * 写锁获取过程 * @param acquires * @return */ protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); //获取线程状态 int w = exclusiveCount(c); //获取写锁状态,及c的低16位表示的值 if (c != 0) { //c != 0 表示有线程持有锁,可能是读锁也可能是写锁 //这个判断返回false,目的是过滤掉读锁持有线程及写锁持有线程不是重入写锁的情况(读写锁是互斥的,写写锁是互斥的) if (w == 0 || current != getExclusiveOwnerThread()) return false; //命中了上面的情况 if (w + exclusiveCount(acquires) > MAX_COUNT) //写锁用state的低16位表示,大于这个值视为非法 throw new Error("Maximum lock count exceeded"); // Reentrant acquire setState(c + acquires); //走到这里的线程必然是写锁,且是写锁重入,所以这个操作不需要原子操作 return true; //获取锁成功 } //走到这里表示目前没有线程持有锁,这里会有锁竞争存在,状态的修改需要原子环境 //writerShouldBlock() 写锁此方法一直返回false //此判断过滤掉竞争锁失败的线程 if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; //设置当前线程为独占线程 setExclusiveOwnerThread(current); return true; }
写锁获取重要分两种情况进行
一 state !=0 主状态不为零,这种情况能够获取写锁的唯一条件是,当前持有锁的线程是写锁,且是写锁重入,其他情况该线程都不可能获得锁
二 state = 0 表示没有任何线程持有锁,当前线程在竞争条件下通过原子操作获取锁,是否成要看运气(不成功则进入阻塞队列)
整个写锁获取围绕上面两个条件进行
/** * 写锁的释放过程 * @param releases * @return */ 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); //设置读写锁的主状态 return free; //true表示所有写线程都释放完毕,反正还有重入锁未释放 }
写锁的释放很简单,第一步判断进入释放方法的线程是否是当前独占锁(进入写锁释放的方法必然要是当前独占锁)
由于存在锁重入情况,当所有重入锁都释放后,返回成功唤醒阻塞队列排在前面的线程,否则防护false 不唤醒任何阻塞队列中的线程
/** * 读锁的释放过程 * 1 读锁与读锁之间是共享锁,即一个读锁持有锁,后面的读锁仍然可以获取读锁 * 2读锁和写锁的关系,读锁和写锁之间是互斥锁,但在获取读锁时,当前线程如果持有写锁,仍然可以获取读锁,当前线程持有读锁则不能获取写锁 * 该思想贯穿整个读锁获取方法 * @param unused * @return */ protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); //获取读写锁总体状态 //写线程持有写锁,且不是持有写锁的线程进行读操作,违反上面介绍的第二条(2) // 除了这种情况,其他情况线程都可以竞争获取线程 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; int r = sharedCount(c); //获取当前持有读锁的数量(state的高16位表示的数值) //走到这里都是可以竞争锁的线程,(当然执行这个方法的线程都是为了获取读锁,这句话是多余的) //readerShouldBlock读锁可以获取线程,阻塞队列head.next不是写锁,r < MAX_COUNT 读锁的数量未达到最大值 // compareAndSetState(c, c + SHARED_UNIT)当前线程获取读锁,状态变更成功 if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { //走到这里表示成获取线程 //该线程第一次获取读锁,未有重入情况 //firstReader,firstReaderHoldCount 记录第一次获取读锁的线程,主要作用是简单缓存 if (r == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { //第一次获取读锁的线程重入啦 firstReaderHoldCount++; //记录重入次数 } else { //处理非第一次获取读锁的线程 //这里也是做缓存,优化性能 // cachedHoldCounter存储最后一次获取读锁的线程 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; } //上面有 return fullTryAcquireShared(current); } /** * readerShouldBlock() 返回 true,2 种情况: 在 FairSync 中说的是 hasQueuedPredecessors(),即阻塞队列中有其他元素在等待锁。 也就是说,公平模式下,有人在排队呢,你新来的不能直接获取锁 在 NonFairSync 中说的是 apparentlyFirstQueuedIsExclusive(),即判断阻塞队列中 head 的第一个后继节点是否是来获取写锁的,如果是的话,让这个写锁先来,避免写锁饥饿。 作者给写锁定义了更高的优先级,所以如果碰上获取写锁的线程马上就要获取到锁了,获取读锁的线程不应该和它抢。 如果 head.next 不是来获取写锁的,那么可以随便抢,因为是非公平模式,大家比比 CAS 速度 compareAndSetState(c, c + SHARED_UNIT) 这里 CAS 失败,存在竞争。可能是和另一个读锁获取竞争,当然也可能是和另一个写锁获取操作竞争。 然后就会来到 fullTryAcquireShared 中再次尝试: */ /** * 抓住再次获取锁的机会 * @param current * @return */ final int fullTryAcquireShared(Thread current) { ReentrantReadWriteLock.Sync.HoldCounter rh = null; for (;;) { //模拟锁自旋操作 int c = getState(); //写锁被占用 if (exclusiveCount(c) != 0) { if (getExclusiveOwnerThread() != current) return -1; //虽然head.next是写锁,但当前读锁是重入锁,就不用等了啦 } else if (readerShouldBlock()) { //这里表示写锁不持有锁,下面只处理读锁重入的情况 if (firstReader == current) { //读锁重入啦 } else { if (rh == null) { rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) { rh = readHolds.get(); //不是重入线程,count= 0 是上面初始化添加的 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; } }
/** * 读锁释放过程 * @param unused * @return */ protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); if (firstReader == current) { //释放第一次获取读锁的线程 // assert firstReaderHoldCount > 0; if (firstReaderHoldCount == 1) //第一次获取读锁firstReader 重入释放完成,置为null firstReader = null; else firstReaderHoldCount--; //自减重入次数 } else { ReentrantReadWriteLock.Sync.HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); //缓存中获取当前线程的重入信息 int count = rh.count; //当前持有读锁线程重入次数 if (count <= 1) { //当前读线程重入释放完成,移除缓存map readHolds.remove(); if (count <= 0) //非法重入数记录 throw unmatchedUnlockException(); } --rh.count; //重入数自减 } for (;;) { //这里模拟线程自旋,保证compareAndSetState(c, nextc)必然会成功 int c = getState(); //读锁重入数自减后,变更读写锁state的状态 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; } }
锁降级
Doug Lea 没有说写锁更高级,如果有线程持有读锁,那么写锁获取也需要等待。
不过从源码中也可以看出,确实会给写锁一些特殊照顾,如非公平模式下,为了提高吞吐量,lock 的时候会先 CAS 竞争一下,能成功就代表读锁获取成功了,但是如果发现 head.next 是获取写锁的线程,就不会去做 CAS 操作。
Doug Lea 将持有写锁的线程,去获取读锁的过程称为锁降级(Lock downgrading)。这样,此线程就既持有写锁又持有读锁。
但是,锁升级是不可以的。线程持有读锁的话,在没释放的情况下不能去获取写锁,因为会发生死锁。
回去看下写锁获取的源码:
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;
...
}
...
}
仔细想想,如果线程 a 先获取了读锁,然后获取写锁,那么线程 a 就到阻塞队列休眠了,自己把自己弄休眠了,而且可能之后就没人去唤醒它了
精彩文章 https://www.javadoop.com/post/reentrant-read-write-lock