ReentrantReadwriteLock,简称RrwLock;
简介
RrwLock实现了ReadWriteLock接口,ReadLock,WriteLock实现了Lock接口规范;
ReadLock共享锁,实现了AQS的tryAcquireShare、tryReleaseShare;
WriteLock独占锁,实现了AQS的tryAcquire、tryRelease;
写操作:与任何读锁互斥,与除自己之外(可重入)的写锁互斥;
读操作:与任何读锁都不互斥,与除自己之外的写锁互斥;
基本组成-类
-
Sync
- HoldCounter:每个线程获取到读锁的计数器
- ThreadLocalHoldCounter:继承ThreadLocal;重写ThreadLocal#initialValue方法,返回HoldCounter
-
FairSync
-
NonfairSync
-
ReadLock
-
WriteLock
Sync 基本属性
- Thread firstReader
记录第一个获取到读锁,且还未释放读锁的线程
- in t firstReaderHoldCount
第一个读锁线程的重入数
- HoldCounter cachedHoldCounter
指向某个持有读锁的线程的HoldCounter对象
- ThreadLocalHoldCounter readHolds
记录每个线程分别持有多少读锁
方法
exclusiveCount
static final int SHARED_SHIFT = 16;
// 10000000000000000 - 1 = 01111111111111111 ===> 16个1
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/**
* 返回共享状态下锁的数量
* c >>> SHARED_SHIFT :
* **************** **************** 将后16位*移走,保留c的前16位*
*/
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/**
* 返回独占状态下锁的数量
*
* 1与任何数进行&操作,都等于任何数本身,0与任何数&操作都等于0
* 参数c为32位* : **************** ****************
* EXCLUSIVE_MASK: 0000000000000000 1111111111111111
* 所以c & EXCLUSIVE_MASK = c的后16位
*/
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
tryAcquire 写锁,独占式获取资源
/**
* Sync#tryAcquire
* 资源已被获取(state != 0):
* 读锁存在,返回false
* 写锁存在但不是当前线程,返回false。写锁可重入
* 超过count最大值(65535),返回false
* 资源未被获取:
* 写阻塞,返回false
* CAS设置状态失败,返回false
*/
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// 获取写锁的状态
int w = exclusiveCount(c);
// 资源已被获取
if (c != 0) {
// 资源被获取并且写锁数量是0,认为是有 读锁 存在
// 读锁存在(读写互斥) 或 当前线程不占有锁,资源获取失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 是否超过最大count限制
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 更新状态
setState(c + acquires);
return true;
}
// 如果需要写阻塞 或 CAS设置状态失败,资源获取失败
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 标记当前线程占有资源
setExclusiveOwnerThread(current);
return true;
}
writerShouldBlock:Sync的抽象方法,FairSync、NonfairSync负责实现
/**
* FairSync#writerShouldBlock
*/
final boolean writerShouldBlock() {
// 调用AQS,查看是否有前一个节点,有的话返回ture,进行阻塞,这样符合公平锁
return hasQueuedPredecessors();
}
/**
* NonfairSync#writerShouldBlock
*/
final boolean writerShouldBlock() {
// 永远不会阻塞
return false;
}
tryRelease 写锁释放资源
/**
* Sync#tryRelease
* 写锁数量为0代表可以释放资源
*/
protected final boolean tryRelease(int releases) {
// 获取到资源的线程是否与当前线程一致
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
// 写锁数量为0代表可以释放资源,free为true
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
tryAcquireShared 读锁,共享式获取资源,
static final int SHARED_SHIFT = 16;
// 10000000000000000 = 65536
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
/*
* Sync#tryAcquireShared,快速获取资源
*
* 满足以下情况可以获取到资源:
* 1. 没有写锁存在 或 本线程的写锁存在
* 2. 不需要读阻塞 且 读锁数量未超过最大值 且 CAS 设置状态成功
*
*/
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 如果有非本线程的写锁存在,返回false (锁降级)
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 获取全部线程的读锁数量
int r = sharedCount(c);
// 不需要读阻塞 且 读锁数量<最大值(65535) 且 CAS设置状态成功,满足这3个条件代表获取锁成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
// CAS设置状态会从65536开始加,因为读锁状态是state前16
compareAndSetState(c, c + SHARED_UNIT)) {
// 读锁数量为0,设置当前线程作为第一个读锁,并初始第一个读锁的count数量 1
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 第一个读锁线程重入,数量+1
firstReaderHoldCount++;
} else {
// 当前线程为第一个读锁线程之外的线程,获取当前线程的HoldCounter
HoldCounter rh = cachedHoldCounter;
// 如果cachedHoldCounter为空或cachedHoldCounter不是当前线程的,
// 则通过ThreadLocal获取一个新的HoldCounter,并将cachedHoldCounter设置为当前线程
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
// 当前线程HoldCounter的count++
rh.count++;
}
return 1;
}
// 获取锁失败,自旋重新获取
return fullTryAcquireShared(current);
}
-------------------------------------------------------------------------
假设有3个线程获取读锁,那么state、读状态、释放锁后的state变化如下:
第一个线程获取到读锁:
state:65536 => 1 0000 0000 0000 0000
读状态:1
释放锁后的state: 65536 - 65536 = 0
第二个线程获取到读锁:
state:131072 => 10 0000 0000 0000 0000
读状态:2
释放锁后的state:131072 - 65536 = 65536
第三个线程获取到读锁:
state:196608 => 11 0000 0000 0000 0000
读状态:3
释放锁后的state:196608 - 65536 = 131072
readerShouldBlock Sync的抽象方法,FairSync、NonfairSync负责实现
/**
* FairSync#readerShouldBlock
*/
final boolean readerShouldBlock() {
// 调用AQS,查看是否有前一个节点,有的话返回ture,进行阻塞,这样符合公平锁
return hasQueuedPredecessors();
}
/**
* NonfairSync#readerShouldBlock
*
* 1. AQS等待队列的第一个节点为写锁,则阻塞当前读锁(只针对新线程),返回false,尽量优先写锁,避免读多写少时,写锁长时间等待;
* 2. 但也不能完全避免写锁长时间等待,如果AQS等待队列的第一个节点为读锁,第二个节点为写锁,那么当前读锁会获取成功,
* 如果读多写少,也会导致第二个写锁一直阻塞。
* 3. 为了防止写锁长时间等待,非公平锁并不是完全的非公平
*/
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
// 调用AQS,判断head节点的下一个节点 是否为独占模式(这里代表写锁),是的话返回true
return apparentlyFirstQueuedIsExclusive();
}
fullTryAcquireShared
/**
* Sync#fullTryAcquireShared
* tryAcquireShared的完整版,处理tryAcquireShare中CAS失败,或需要读阻塞的情况
*
* 返回-1,结束自旋的情况:
* 1. 有非本线程的写锁存在
* 2. 需要读阻塞,但当前线程不可重入(HoldCounter.count == 0)
*/
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
// 第一部分:筛选不符合条件的线程,返回-1,结束自旋
int c = getState();
// 判断是否有非本线程的写锁存在,如果有返回-1
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// 判断是否需要读阻塞
// 就算需要读阻塞,但如果线程已经获取到了读锁,也是可以继续重入,不需要返回-1,再次排队
} else if (readerShouldBlock()) {
// 如果当前线程是firstReader且不为null,说明已经获取到了读锁,可继续重入
if (firstReader == current) {
} else {
// 当前线程不是firstReader,则获取线程对应的HoldCounter:通过缓存或ThreadLocal
if (rh == null) {
rh = cachedHoldCounter;
// 如果缓存的不是当前线程的HoldCounter,则从ThreadLocal获取
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
// rh.count == 0说明当前线程第一次获得读锁,在读阻塞下,要remove
// 否则说明rh.count > 0,已获取到读锁,可重入
if (rh.count == 0)
readHolds.remove();
}
}
// 当前线程第一次获得读锁,返回-1
if (rh.count == 0)
return -1;
}
}
// 第二部分:CAS设置资源状态,失败后进行循环
// 是否超过读锁最大限制
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// CAS设置state
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 设置第一个获取到读锁的线程,读锁数量
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
// 如果是当前线程是第一个获取读锁线程,读锁数量++
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
// 获取缓存的cacheHold
if (rh == null)
rh = cachedHoldCounter;
// 如果当前缓存的cacheHold不是当前线程的,初始化一个新的HoldCounter
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
// 将当前线程的读锁count +1
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
tryReleaseShare 读锁释放资源
/*
* Sync#tryReleaseShared
* 读锁跟写锁都为0,才认为释放完成
*
*/
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 当前线程是第一个获取到读锁的线程
if (firstReader == current) {
// ==1,代表没被重入,并设为null,代表第一个被占用的线程已被释放
if (firstReaderHoldCount == 1)
firstReader = null;
else // 代表被重入,count--
firstReaderHoldCount--;
} else {
// 从缓存跟ThreadLocal中获取其它线程的HoldCounter
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
// 代表没被重入,可以被释放了,从ThreadLocal中删除
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
// 代表被重入,--count
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// 读锁跟写锁都为0,才认为释放完成
return nextc == 0;
}
}
知识点
HoldCounter中使用线程id,而不是线程引用,是为了防止影响垃圾回收?
ThreadLocal中key为Thread,而HoldCounter再引用Thread,会造成互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价【待研究】),所以其实这样做只是为了帮助GC快速回收对象而已。
ReadLock、WriteLock、或者是ReentrantLock 为什么不继承AQS,而是要Sync去继承能?
让Sync单独继承AQS, 可以让每个类的指责更明显,类更好维护
为什么某些字段要用 transient 修饰?
- private transient ThreadLocalHoldCounter readHolds;
- private transient HoldCounter cachedHoldCounter;
- private transient Thread firstReader;
- private transient int firstReaderHoldCount;
从被修饰的字段可以看出,都是记录线程相关的信息(重入数、线程id), transient是防止被序列化,线程这种执行完就没了,如果持久化后再反序列化返回会造成不必要的麻烦,比如:无法释放锁。
RrwLock什么时候会锁降级?
获取读锁时,如果有当前线程的写锁存在,也可以继续获取读锁,当写锁释放时,写锁会变为读锁,这就是锁降级;
如果当前线程的读写锁同时存在,需要写锁释放后,读锁才会成功释放;
锁降级Demo分析:
public static void main(String[] args) {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
Lock writeLock = reentrantReadWriteLock.writeLock();
Lock readLock = reentrantReadWriteLock.readLock();
writeLock.lock();
readLock.lock();
writeLock.unlock();
readLock.unlock();
}
获取写锁时,state:1,写锁状态:1,读锁状态:0
获取读锁时,state:65537,写锁状态:1,读锁状态:1
释放写锁时,state:65536,写锁状态:0,读锁状态:1 ,这时候变成读锁了,锁降级
释放读锁时,state:65536,写锁状态:0,读锁状态:0
NonfairSync#readerShouldBlock中, 为什么AQS等待队列的首节点为写锁,则阻塞当前读锁?
- 避免非公平读锁下的写饥饿问题(写锁一直获取不到资源),公平锁不会存在问题,等待队列是FIFO执行,总会被唤醒;
- 读锁是共享锁,只要不超过最大限制65535,就可以一直获取, 如果读多写少的话,可能导致等待队列首节点写锁一直无法被唤醒;
- 如果首节点是读锁a,读锁a下一个是写锁a,这样仍无法避免写锁a的写饥饿问题。
StampedLock 替代 RrwLock?
【待研究】
RrwLock跟ReentrantLock相比有哪些优势?
RrwLock 将读、写拆分,增加了读操作的效率,不需要将读,写串行执行
为什么读锁需要记录每个线程的读锁count,其它并发锁不需要能?
ReentrantLock:独占模式,AQS.state可以表示当前线程重入数;
Semphore:共享模式,不关注每个线程重入次数,只需关注还剩多少资源,可以被获取;
CountDownLatch: 共享模式,不关注每个线程重入次数,只需关注还剩多少资源,没被释放;
RrwLock:读锁-共享模式,在#fullTryAcquireShare中,如果是读阻塞,需要根据线程重入次数(HoldCounter.count),判断当前线程是新线程还是老线程,读阻塞时新线程不允许获取。
为什么RrwLock中第一个线程不放到ThreadLocal中,而要定义firstThread?
为了提高性能,减少从ThreadLocal中读取的次数,包括cacheHoldCounter也是为了提高性能。
之前1.5的时候没有firstThread,1.6之后有人提了个bug,ThreadLocal内存溢出了,ThreadLocal没有remove,后来解决bug的时候,优化了这个。 参考:blog.csdn.net/weixin_3658…