文章目录
前言
ReentrantReadWriteLock类不仅仅只使用了某一种锁(独占锁或共享锁),而是使用了两种独占锁和共享锁,读的时候允许多个线程同时读,写的时候那其他线程就得乖乖等待了。
1、重要成员
1.1、内部类关系
public class ReentrantReadWriteLock implements ReadWriteLock {
/** 读锁 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 写锁 */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** 持有的AQS子类对象 */
final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}
public static class ReadLock implements Lock {}
public static class WriteLock implements Lock {}
}
1.2、构造方法
public ReentrantReadWriteLock() {
//m默认非公平锁
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
public static class ReadLock implements Lock {
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
}
public static class WriteLock implements Lock {
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
}
ReentrantReadWriteLock的构造器默认使用非公平锁。在ReentrantReadWriteLock的构造器中又会去构造ReadLock和WriteLock,从这二者的构造器中可见,它持有的AQS对象是同一个,也就是ReentrantReadWriteLock的AQS成员。重点在于,ReadLock和WriteLock使用的同一个AQS对象,使得可以读写互斥。
1.3、Sync的成员
abstract static class Sync extends AbstractQueuedSynchronizer {
static final int SHARED_SHIFT = 16;
//2^16
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//最大共享值,2^16-1
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//判断独占锁的标志 2^16-1
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count */
//
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
}
1.3.1、读锁计数
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
/**
* ThreadLocal subclass. Easiest to explicitly define for sake
* of deserialization mechanics.
*/
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
private transient ThreadLocalHoldCounter readHolds;
//历史上第一个获取读锁的线程
private transient Thread firstReader = null;
//历史上第一个获取读锁的线程的计数器,该线程重入的次数
private transient int firstReaderHoldCount;
//缓存了线程HoldCounter对象
private transient HoldCounter cachedHoldCounter;
回想一下Semaphore对共享锁的操作,获取共享锁时Semaphore不会去记录是哪个线程拿到了共享锁,释放共享锁时不管是哪个阿猫阿狗都可以来释放共享锁。
但是read锁不是这样的,他不仅仅会记录线程获得几次锁(包括读锁和写锁记录在state),还会记录每个线程获得了几次读锁。
现在需要记录各个线程分别拿走了多少读锁,我们把记录工作交给各个线程自己,通过ThreadLocal让每个线程拥有一个线程私有的HoldCounter对象。如果当前线程没有持有读锁,这个HoldCounter对象为null(因为对ThreadLocal没有使用过get/set);如果当前线程持有着读锁,这个HoldCounter对象不为null,且count成员肯定大于等于1。
cachedHoldCounter / firstReader / firstReaderHoldCount存在的理由,仅仅是为了获得当前线程的HoldCounter对象的一次快速尝试,如果快速尝试失败了,才需要通过ThreadLocal来获得当前线程的HoldCounter对象。
- 当读锁的计数器为1时,读锁计数器就是通过sharedCount方法计算的,这时firstReader 要么为历史上第一个获取读锁的节点的线程,要么为null(firstReader已经释放完锁了),当firstReader 释放干净读锁后,这时firstReader 还是为null,因为只有释放完所有的读锁后,某线程再次获取读锁,这时firstReader 为该线程。firstReader 作用并不大,只是快速获取读锁信息的以中途径,不然还是可以通过ThreadLocal来获取的。
- cachedHoldCounter一般情况下,这个引用总是指向某个持有读锁的线程的HoldCounter对象。但cachedHoldCounter当好是当前线程的HoldCounter对象这种事情,则完全看缘分(后面会讲到)。
2、写锁的获取和释放
2.1、写锁的获取
WriteLock方法 | 调用的AQS方法 | 是否阻塞 | 是否响应中断 | 是否超时机制 | 返回值及含义 |
---|---|---|---|---|---|
lock() | sync.acquire(1) | ✓ | - | - | void |
lockInterruptibly() | sync.acquireInterruptibly(1) | ✓ | ✓ | - | void |
tryLock(long timeout, TimeUnit unit) | sync.tryAcquireNanos(1, unit.toNanos(timeout)) | ✓ | ✓ | ✓ | boolean 返回时是否获得了锁 |
tryLock() | sync.tryWriteLock() | - | boolean 返回时是否获得了锁 |
写锁就是独占锁,我们只需要分析tryAcquire的重写即可,后面的阻塞机制就是AQS内部的方法了。
public static class WriteLock implements Lock {
private final Sync sync;
public void lock() {
sync.acquire(1);
}
...
}
从上面的sync.acquire(1)
出发,会调用到子类的tryAcquire
实现。在此之前,回顾一下tryAcquire
返回值的含义,若返回true代表获取独占锁成功,若返回false代表获取独占锁失败。
protected final boolean tryAcquire(int acquires) {
//获得当前线程
Thread current = Thread.currentThread();
//获得同步器状态
int c = getState();
//获得写锁计数
int w = exclusiveCount(c);
//如果c不为0,说明有锁,但不知道是什么锁
if (c != 0) {
// 进入分支有两种情况:
// 1.写锁计数为0。说明此时只有读锁,不能将读锁升级为写锁,所以直接返回false,这儿有个小知识点
如果同一个线程先获取读锁在获取写锁是不可以的,若先获取写锁再获取读锁是可以的。
// 2.写锁计数不为0,但不是当前线程持有的写锁。直接返回false。
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 加上参数的写锁计数,如果溢出了,就抛出异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 执行到这里,说明肯定是当前线程持有的写锁,那么此时没有线程竞争,
// 直接set新的写锁计数
setState(c + acquires);
return true;
}
// 执行到这里,肯定是c == 0,当前既没有读锁,也没有写锁。
// 但可能有多个线程来竞争这个状态下的任何锁,所以接下来需要通过CAS来竞争
// writerShouldBlock方法对公平非公平进行了封装,非公平直接返回false,公平锁判断是否存在阻塞的节点
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))// 如果CAS成功,则不会进入此分支
return false;
//执行到这里,说明该函数开始检测到 没有任何锁,然后当前线程还获得到了写锁
setExclusiveOwnerThread(current);
return true;
}
具体细节请看注释。我们知道写锁和写锁肯定互斥,写锁也和读锁互斥,所以上面直接返回false的情况挺多的,所以我们不如先说一下返回true的情况(按照程序中的顺序):
-
之前是由当前线程持有的写锁,所以当前线程现在重入这个写锁。
-
当前ReentrantReadWriteLock没有任何锁被持有,并且当前线程竞争到了写锁。
直接返回false的情况(按照程序中的顺序):
-
当前只有读锁,不能将读锁升级为写锁。
-
当前有写锁,但写锁的持有者不是当前线程。
-
当前没有任何锁,但判断公平模式后发现当前线程排在了其他线程后面。
-
当前没有任何锁,判断公平模式后发现可以直接尝试,但CAS竞争失败了。
writerShouldBlock这个函数封装掉了 当前是公平还是非公平 的信息,我们只需要知道该函数返回了false,接下来就可以尝试获得写锁;返回了true,接下来不能去尝试获得写锁,且即将进入阻塞状态(详见AQS#acquire)。
而返回false有两种可能性:
-
该锁的实现允许插队(即非公平实现)。
-
当前线程排在了队伍的最前面(即公平实现,但此时同步队列中没有等待的线程)。
接下来看一下AQS子类的新加方法tryWriteLock
,非公平的、一次性的获取写锁的方法实现:
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c != 0) {
int w = exclusiveCount(c);
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
if (!compareAndSetState(c, c + 1))
return false;
setExclusiveOwnerThread(current);
return true;
}
发现它和上面的tryAcquire方法实现几乎一样,除了:
-
该函数没有参数,只会让写锁计数加1。
-
CAS操作前,没有判断writerShouldBlock。这就是非公平的体现。
-
判断溢出变得简单,因为只是加1,所以旧值如果刚好等于最大值,那么再加1肯定溢出。
-
即使是重入写锁(没有线程竞争),也是使用CAS操作增加写锁计数。
2.2、写锁的释放
public static class WriteLock implements Lock {
private final Sync sync;
public void unlock() {
sync.release(1);
}
}
同样也只是关注tryRelease重写方法即可,之后的过程与AQS独占锁释放一样。
protected final boolean tryRelease(int releases) {
//要释放写锁,首先得保证当前线程已经持有了写锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//计算出同步状态的新值
int nextc = getState() - releases;
//如果新值的写锁的重入次数为0,那么写锁将被释放
//free为0说明写锁释放完了,不为0说明该线程写锁重入了
boolean free = exclusiveCount(nextc) == 0;
if (free)
//如果写锁将完全释放,那么设置ExclusiveOwnerThread成员为null
setExclusiveOwnerThread(null);
//不管新值是多少,设置它为state
setState(nextc);
return free;
}
3、读锁的获取和释放
3.1、读锁的获取
ReadLock方法 | 调用的AQS方法 | 是否阻塞 | 是否响应中断 | 是否超时机制 | 返回值及含义 |
---|---|---|---|---|---|
lock() | sync.acquireShared(1) | ✓ | - | - | void |
lockInterruptibly() | sync.acquireSharedInterruptibly(1) | ✓ | ✓ | - | void |
tryLock(long timeout, TimeUnit unit) | sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)) | ✓ | ✓ | ✓ | boolean 返回时是否获得了锁 |
tryLock() | sync.tryReadLock() | - | boolean 返回时是否获得了锁 |
共享锁的获取前面已经讲解过了,所以接下来我们只需要关心AQS子类对tryAcquireShared和
tryReleaseShared的重写实现即可。
public static class ReadLock implements Lock {
private final Sync sync;
public void lock() {
sync.acquireShared(1);
}
...
}
回顾一下,tryAcquireShared的返回值的情况:
-
返回>0:说明共享锁获取成功,并且后续线程也可能获取。
-
返回0:说明共享锁获取成功,后续线程大概率失败。
-
返回<0:说明共享锁获取失败。
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
1、获取线程共享资源
int c = getState();
2、如果当前写锁被持有&&当前线程不是写锁的线程,直接返回-1,因为读写互斥
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
3、获取读锁持有的数量
int r = sharedCount(c);
4、readerShouldBlock判断读锁是否被阻塞,在非公平与公平锁被重写。
4.1、公平锁就是判断前面是否存在等待的节点
4.2、非公平锁判断head的后继即第一个阻塞的节点是否是写锁
if (!readerShouldBlock() &&
r < MAX_COUNT && // 读锁是否小于最大值2^16-1
compareAndSetState(c, c + SHARED_UNIT)) { //cas设置state
5、到这儿说明设置成功,其实已经持有读锁了,这儿是一些善后操作,
就算没有这些操作也完全可以运行的
5.1、读锁为0,说明还没有线程持有读锁,当前线程是第一个获取读锁的
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
5.2、firstReader就是当前线程,说明该线程重入了读锁,直接设置数量+1即可
} else if (firstReader == current) {
firstReaderHoldCount++;
5.3、说明当前线程不是最早获取读锁的线程
} else {
HoldCounter rh = cachedHoldCounter;
6、判断当前线程是否是cachedHoldCounter缓存中的,不是则把rh设置成当前线程的持有者
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
6.1、到这儿说明rh肯定被前线程持有了,
若count==0说明该线程是第一次获取锁,则设置到ThreadLocal中
else if (rh.count == 0)
readHolds.set(rh);
6.2、线程获取读锁数量+1
rh.count++;
}
return 1;
}
7、执行到这儿说明第一次获取读锁失败,进入自旋操作尝试获取读锁
return fullTryAcquireShared(current);
}
-
从最开始return -1的地方可知,获取读锁会因为当前写锁不是当前线程所持有而直接返回-1。但获取读锁允许写锁是当前线程所持有而继续尝试获得。
-
在CAS操作compareAndSetState(c, c + SHARED_UNIT)执行成功后,说明当前线程获取共享锁成功,但还需要做一系列的善后操作。
疑问:为什么firstReader、firstReaderHoldCount、cachedHoldCounter没有采用CAS的方式设置而是直接设置,多线程中不会有问题吗?
只有当state为0时,历史上第一个线程才会去设置firstReader以及firstReaderHoldCount,后续线程无法设置这两个属性,所以不存在线程竞争关系。
cachedHoldCounter在线程中属性会特别的混乱,cachedHoldCounter这个值可以被所有的线程设置,所以说他属于哪个线程会特别的混乱,但若恰好是cachedHoldCounter是当前线程,则不会去threadLocal中去读取了,即使cachedHoldCounter非常混乱,但执行到cachedHoldCounter = rh = readHolds.get();时,已经保证了局部变量rh一定是当前线程的,不需要在关注cachedHoldCounter属于哪个线程,所以也不会存在线程安全的问题,其实就是尝试一次获取,是当前线程就是运气好。
下面进入fullTryAcquireShared自旋进行尝试获取锁:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
//第一部分
int c = getState();
1、同样判断读写互斥
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
2、进入这儿说明当前线程获取锁可能失败了
if (firstReader == current) {
2.1、当前线程是重入的,则执行第二部分,设置state,因为此时线程已经获取到锁了
} else {
3、第一次进入rh肯定为null,这样就可以保证rh是当前线程的
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
3.1、该线程读锁计数为0,说明是初始化的,第一次获取读锁,从ThreadLocal移除掉
若不是0,说明是重入读锁,跳过直接执行第二部分尝试获取锁
if (rh.count == 0)
readHolds.remove();
}
}
3.2、对应前面的,直接返回-1,尝试获取锁失败
if (rh.count == 0)
return -1;
}
}
//第二部分
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
4、通过cas设置state,成功说明已经获取到锁了,和前面一样做一些善后操作,不在赘述
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;
}
}
}
我们把自旋的逻辑分为两个部分:
- 第一部分负责判断当前线程符不符合继续获得锁的条件,如果不符合则返回-1退出自旋;如果符合,则继续执行第二部分。
- 第二部分负责CAS修改同步器的状态,如果修改成功,则继续完成善后操作;如果修改失败,继续下一次循环。
-
if (firstReader == current)分支进入,说明firstReader不为null,从读锁的释放过程来看,只要firstReader不为null,那么firstReaderHoldCount肯定大于0。既然大于0,说明当前线程是在重入读锁,所以给当前线程放行,继续执行第二部分。
-
if (firstReader == current)的else分支进入,说明当前线程不是firstReader,看来没法通过方便的firstReader来判断,只能依靠其他东西。
-
如果rh为null,获取到当前线程的HoldCounter对象作为赋值给rh。从整个函数逻辑来看,局部变量rh只要不为null,就肯定是当前线程的HoldCounter对象。重点在于,只要执行到if (rh.count == 0)(指第一条)时,rh就已经是当前线程的ThreadLocal的HoldCounter对象了。
-
这里需要分两种情况(重入读锁、第一次获取读锁),如果是第一次获取读锁这种情况,那么执行readHolds.get()之前,当前线程是没有HoldCounter对象的(这一点可以从读锁的释放过程得知)。所以readHolds.get()得到的肯定是一个初始的HoldCounter对象,count肯定为0,发现是这种情况,则需要及时清空当前线程的HoldCounter对象(readHolds.remove()),以维持“没有持有读锁时,线程肯定没有ThreadLocal的HoldCounter对象”的规则。接下来第二个if (rh.count == 0)判断会成立就会直接退出循环了。
-
如果是重入读锁这种情况,那么执行readHolds.get()之前,当前线程是拥有HoldCounter对象的,且count肯定是大于0的。接下来第二个if (rh.count == 0)判断,也不会进入。所以会顺利执行到第二部分。
-
tryReadLock实现和前面类似,不在解析。
3.2、读锁的释放
同样只关注tryReleaseShared方法即可
public static class ReadLock implements Lock {
private final Sync sync;
public void unlock() {
sync.releaseShared(1);
}
}
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
1、如果当前线程是firstReader,修改对应的信息
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
2、反之从ThreadLocal获取读锁信息,减去对应的持有锁数量
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
2.1、当前线程读取锁的次数
int count = rh.count;
if (count <= 1) {
2.2、count为1说明即将释放完该线程的读锁
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
2.3、count-1
--rh.count;
}
for (;;) {
3、通过自旋设置state
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
3.1、成功之后,若已经释放完所有的读写锁,返回true,否则返回false
这儿和Semaphore信号量有所区别
return nextc == 0;
}
}
-
如果firstReader为null,说明历史上第一个reader已经完全释放干净读锁了。反之,无法通过firstReaderHoldCount == 1推导出firstReader不为null。
-
返回的是nextc == 0,只有在读写锁都是干净的情况,才返回true。这里有点疑问是,在以前讲解的共享锁的释放过程中,是一定要让tryReleaseShared返回true以便接下来调用doReleaseShared来唤醒后面的共享锁节点,难道当前线程释放读锁后,因为别的线程还持有着读锁,所以还是得返回false?
-
之所以这么做,是因为读锁在获取过程中由于读读不互斥所以基本不会阻塞等待(指当前写锁没有被其他线程持有的情况),而且就算同步队列中有连续的几个共享锁,唤醒后面共享锁节点的任务都在 共享锁获取成功时就做掉了(AQS中setHeadAndPropagate调用了doReleaseShared方法唤醒后续节点)。所以读锁释放成功时,一般不需要返回true。
-
而返回的true的情况是,释放读锁后,当前读写锁都是干净的,这个时候来唤醒写锁节点才合适。因为写 和读写 都是互斥的。
-
综上,tryReleaseShared返回true的原因是,为了唤醒写锁节点,在当前读写锁都没被持有的情况下。
-
4、锁降级
从本文的分析来看,一个线程持有写锁后,可以继续去持有读锁,如果在这之后,这个线程释放了写锁,那么就称写锁现在降级为了读锁。
上面这个过程,细说的话,应该分为两个部分:
-
一个线程持有写锁后,继续去持有读锁——锁的重入。
-
同时持有读写锁后,先释放了写锁——锁降级。
在上面fullTryAcquireShared的讲解中,解释了“一个线程持有写锁后,可以继续去持有读锁”的必要性,如果不允许继续去持有读锁,转而进入阻塞等待的过程,会造成死锁的。
如果一个线程持有了读锁,不能继续去持有写锁,从而锁升级。因为可能当前不止有一个线程都持有了读锁,你再去获得写锁是不合理的。
5、总结
-
同步器的state被划分为两个部分,分别记录被拿走的读锁和写锁的总数。
-
分别记录各个线程拿走的读锁的工作交给了各个线程自己,通过ThreadLocal实现。
-
不仅写锁可以重入(这类似于ReentrantLock),读锁也可以重入。
-
尝试获取写锁时,会因为其他写锁或任意读锁(包括自己)的存在,而进入阻塞等待的过程,抛入sync queue中去。
-
尝试获取读锁时,会因为其他写锁(不包括自己的写锁)的存在,而进入阻塞等待的过程,抛入sync queue中去。
-
读锁的非公平获取中,apparentlyFirstQueuedIsExclusive 一定概率防止了写锁无限等待。
-
锁降级是指,一个线程同时持有读锁和写锁后,先释放了写锁,使得写锁降级为了读锁。
参考链接:https://blog.csdn.net/anlian523/article/details/106955678