什么是ReentrantReadWriteLock?
传统的锁,比如synchronized或者ReentrantLock,无论是读还是写,同一时间只允许一个线程访问共享资源。
当读操作远多于写操作时,这种互斥会导致性能瓶颈,而多个读操作本身不会修改数据,完全可以并发执行。ReentrantReadWriteLock使得多个线程可以同时获得读锁,使得并发效率提高:
-
读-读不互斥:多个线程可以同时获取读锁
-
读-写互斥:有线程持有写锁时,其他线程无法获取读锁或写锁
-
写-写互斥:同一时间只允许一个线程持有写锁
ReentrantReadWriteLock的核心特性
读写锁分离
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReadLock readLock = rwLock.readLock(); // 读锁
WriteLock writeLock = rwLock.writeLock(); // 写锁
其中读锁不支持条件变量,写锁支持条件变量。
重入性
-
读锁:允许同一线程重复获取读锁
-
写锁:允许同一线程重复获取写锁(可重入)
-
写锁可以降级为读锁,但读锁不能升级为写锁
当一个线程获取了读锁之后,再去获得写锁,会导致获取写锁永久等待。 当一个线程获取了写锁之后,再去获得读锁,可以成功获取。
公平性
// 非公平锁(默认)
ReentrantReadWriteLock nonfairLock = new ReentrantReadWriteLock();
// 公平锁
ReentrantReadWriteLock fairLock = new ReentrantReadWriteLock(true);
内部的State含义
/*
* Read vs write count extraction constants and functions.
* Lock state is logically divided into two unsigned shorts:
* The lower one representing the exclusive (writer) lock hold count,
* and the upper the shared (reader) hold count.
*/
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 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; }
ReentrantReadWriteLock中的state用于同时维护读锁和写锁的状态:
-
高16位:记录读锁的持有数量
-
低16位:记录写锁的重入次数
读锁
加锁流程
tryAcquireShared
public void lock() {
sync.acquireShared(1);
}
读锁本质是一种共享锁,因此调用的是acquireShared
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
acquire(null, arg, true, false, false, 0L);
}
其内部先调用tryAcquireShared,如果未获取到锁再进入同步队列。
protected final int tryAcquireShared(int unused) {
//获取当前的线程
Thread current = Thread.currentThread();
int c = getState();
//如果有线程持有写锁并且该线程并不是本线程,则获取读锁失败,这里实现了读写互斥
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//获取读锁被持有的数量
int r = sharedCount(c);
if (!readerShouldBlock() && //判断读线程是否应该阻塞
r < MAX_COUNT && //判断读锁是否超限
compareAndSetState(c, c + SHARED_UNIT)//尝试CAS
) {
if (r == 0) { //如果是第一个读锁
firstReader = current; //记录这个线程
firstReaderHoldCount = 1; //并记录当前线程持有的读锁数量
} else if (firstReader == current) {
firstReaderHoldCount++; //增加线程持有的读锁数量
} else {
HoldCounter rh = cachedHoldCounter;//获取上一个非firstReader线程的缓存
//如果该缓存为空或者缓存的线程和当前线程不同
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
//获得或者创建当前线程的HoldCounter,并且赋给cachedHoldCounter
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)//count==0说明是新创建的
readHolds.set(rh);//将新创建的放入
rh.count++;//增加一个读锁的持有
}
return 1;//获取到读锁
}
return fullTryAcquireShared(current);
}
总结上述源码的流程:
- 判断是否有线程持有写锁,且该线程是否是当前线程,实现了读写互斥和锁降级。
- 判断是否可以尝试获取锁
readerShouldBlock判断读线程是否应该阻塞。由公平/非公平策略实现的方法:- 锁的数量有没有越界
非公平锁:通常检查同步队列中是否有正在等待的、比自己更早的线程(主要是写线程)。如果队列头结点的下一个节点是请求写锁的线程,读线程可能会阻塞,以减少“写线程饥饿”的概率。
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null // 头节点存在
&& (s = h.next) != null //有第一个等待者
&&!(s instanceof SharedNode) // 不是读线程
&& s.waiter != null; //关联的线程不为null
}
为什么是可能阻塞?
因为上述的策略中,只检查第一个等待者是不是写线程,可能会出现第一个是读线程,后续的才是写线程的情况。
比如此刻一个线程获得了写锁,后续5个读线程被阻塞在同步队列中
head->(线程1)->(线程2)->(线程3)->(线程4)->(线程5)
这时又来一个写线程,因为写写互斥,因此也阻塞
head->(线程1)->(线程2)->(线程3)->(线程4)->(线程5)->(写线程6)
当当前线程释放写锁,唤醒线程1,因此线程1是共享节点,当它获得锁之后,会唤醒后续的读线程2,发生连锁反应,而如果在这个反应的过程中,新来了一个读线程7,此时写线程6还不是head的后继节点,因此这时线程7会插队,直接获得锁。
公平锁:检查同步队列中是否有任何正在等待的线程(无论读写)。如果有,则新来的读线程需要排队,保证绝对公平。
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
- 最后尝试一次
CAS,如果CAS失败,将调用fullTryAcquireShared - 如果
CAS成功- 判断当前线程是否是第一个读锁,如果是使用
firstReader记录,并设置firstReaderHoldCount=1或者增加1 - 如果不是,则判断是否是上一个非
firstReader线程的HoldCounter的缓存 - 如果都不是,通过
ThreadLocal获取当前线程的HoldCounter,更新持有数量
- 判断当前线程是否是第一个读锁,如果是使用
firstReader的作用是什么?
它的主要作用是记录第一个获取读锁的线程,并缓存该线程的重入次数。其核心设计目的是为了避免在某些高频场景下使用相对昂贵的 ThreadLocal查找,从而提升性能。
cachedHoldCounter是什么?
cachedHoldCounter是ReentrantReadWriteLock中另一个性能优化字段,它与firstReader协同工作,共同目标是减少访问ThreadLocal的次数,从而提升锁操作的效率。
以下是HoldCounter以及ThreadLocalHoldCounter的源码
static final class HoldCounter {
int count; // initially 0
// Use id, not reference, to avoid garbage retention
final long tid = LockSupport.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();
}
}
fullTryAcquireShared
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
//存在写线程,且写线程不是当前线程
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
/**
* 这部分代码都在判断当前线程是否已经持有读锁,正在重入
*/
if (firstReader == current) {
//当前线程是第一个读线程,因此允许重入,不阻塞
} else {
if (rh == null) {
rh = cachedHoldCounter;//获取缓存
if (rh == null ||
rh.tid != LockSupport.getThreadId(current)) {
//缓存不是当前线程的,获取当前线程的缓存
rh = readHolds.get();
if (rh.count == 0) //计数为0,清理ThreadLocal
readHolds.remove();
}
}
//计数为0,说明不持有读锁,阻塞
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//CAS获取读锁,这部分逻辑和tryAcquireShared一样
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 != LockSupport.getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
fullTryAcquireShared和tryAcquireShared的代码部分类似。
总结上述源码的流程:
- 判断是否有线程持有写锁,且该线程是否是当前线程,实现了读写互斥和锁降级。
- 由
readerShouldBlock判断是否应该阻塞,需要阻塞的情况下,再判断是否是重入情况 CAS获取锁,更新计数。
acquire中获取锁
如果是在同步队列中,该线程成为头节点,获取锁,还会执行
if (acquired) {
if (first) {
node.prev = null;
head = node;
pred.next = null;
node.waiter = null;
if (shared)
signalNextIfShared(node);
if (interrupted)
current.interrupt();
}
return 1;
}
private static void signalNextIfShared(Node h) {
Node s;
if (h != null && (s = h.next) != null &&
(s instanceof SharedNode) && s.status != 0) {
s.getAndUnsetStatus(WAITING);
LockSupport.unpark(s.waiter);
}
}
这部分代码,如果后继节点是共享节点,那么该节点会被唤醒,形成连锁反应,直到遇到一个独占节点。
释放锁流程
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
signalNext(head);
return true;
}
return false;
}
这是两个经典的方法了,重点看重写的tryReleaseShared方法。
protected final boolean tryReleaseShared(int unused) {
//获取当前线程,判断是否是firstReader
Thread current = Thread.currentThread();
if (firstReader == current) {
if (firstReaderHoldCount == 1)
//完全释放,firstReader置为null
firstReader = null;
else
firstReaderHoldCount--;
} else {
//获得当前的HolderCounter
HoldCounter rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
//不是cachedHoldCounter,从ThreadLocal中取出
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
//完全释放读锁,在ThreadLocal中移除
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
//持有数量减一
--rh.count;
}
//自旋cas更新state
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
//如果nextc==0,代表读锁和写锁全部被释放了,可以尝试唤醒写线程了
return nextc == 0;
}
}
总结上述源码的流程:
- 通过
firstReader以及cachedHoldCount获得当前线程的HoldCount,然后更新其中的计数 - 通过自旋
CAS来更新State,并且只有在state==0即没有任何线程拥有读写锁,才会唤醒同步队列中的线程。
写锁
加锁流程
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg))
acquire(null, arg, false, false, false, 0L);
}
重点在tryAcquire方法。
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
////这种情况代表当前有线程拥有锁
if (c != 0) {
// c!=0&&w==0代表当前有读锁,直接获取失败
// c!=0&&w!=0代表当前有写锁,如果写锁不是当前线程的,也直接失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//下面是重入锁的处理
//重入次数越界
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//直接更新锁状态
setState(c + acquires);
return true;
}
//这种情况代表当前没有线程拥有锁
if (writerShouldBlock() //判断写线程是否应该阻塞
|| !compareAndSetState(c, c + acquires)//cas更新锁
)
return false;
//获取锁成功,设置独占线程
setExclusiveOwnerThread(current);
return true;
}
总结上述源码的流程:
1.判断当前是否有线程持有锁,如果持有锁,继续判断
- 判断持有的是否是读锁,如果是读锁,则直接获取失败(读写互斥,且读锁不能升级)
- 判断持有写锁的线程是否是当前线程,不是则获取失败,是则更新
state
2.当没有线程持有锁时,通过writerShouldBlock判断是否阻塞
非公平锁实现
永远返回false
final boolean writerShouldBlock() {
return false; // writers can always barge
}
公平锁实现
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
public final boolean hasQueuedPredecessors() {
Thread first = null; Node h, s;
if ((h = head) != null && ((s = h.next) == null ||
(first = s.waiter) == null ||
s.prev == null))
first = getFirstQueuedThread(); // retry via getFirstQueuedThread
return first != null && first != Thread.currentThread();
}
- 通过
CAS获取锁
释放锁流程
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
signalNext(head);
return true;
}
return false;
}
tryRelease的源码如下:
protected final boolean tryRelease(int releases) {
//判断是否是当前线程持有的
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
//如果完全释放了,独占线程设置为null
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}