1、如何利用 AQS 的state记录读锁和写锁持有状态
ReentrantReadWriteLock 利用 state 的高低16位来区分持有读锁和写锁。
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; }
2、多线程持持有读锁,如何记录分别的持有读锁次数
/**
* firstReader is the first thread to have acquired the read lock.
* firstReaderHoldCount is firstReader's hold count.
*
* <p>More precisely, firstReader is the unique thread that last
* changed the shared count from 0 to 1, and has not released the
* read lock since then; null if there is no such thread.
*
* <p>Cannot cause garbage retention unless the thread terminated
* without relinquishing its read locks, since tryReleaseShared
* sets it to null.
*
* <p>Accessed via a benign data race; relies on the memory
* model's out-of-thin-air guarantees for references.
*
* <p>This allows tracking of read holds for uncontended read
* locks to be very cheap.
*/
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
/**
* The hold count of the last thread to successfully acquire
* readLock. This saves ThreadLocal lookup in the common case
* where the next thread to release is the last one to
* acquire. This is non-volatile since it is just used
* as a heuristic, and would be great for threads to cache.
*
* <p>Can outlive the Thread for which it is caching the read
* hold count, but avoids garbage retention by not retaining a
* reference to the Thread.
*
* <p>Accessed via a benign data race; relies on the memory
* model's final field and out-of-thin-air guarantees.
*/
private transient HoldCounter cachedHoldCounter;
/**
* ThreadLocal subclass. Easiest to explicitly define for sake
* of deserialization mechanics.
*/
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
成功持有锁,必须先利用 CAS 对 state 的高16位加1,然后接着有下面的操作,分别是记录每个线程持有多少次锁。
1、如果第一个成功持有锁第一次成功持有锁,那么会将 firstReader 指向当前线程,并且给 firstReaderHoldCount 赋值1,后续该线程重复获取读锁,就直接对 firstReaderHoldCount 进行 ++ 操作。
2、后续有其他线程来获取读锁,如果判断 firstReader 不是自己,那么就会去获取 (HoldCounter)cachedHoldCounter【它用于记录最后一个成功获取读锁的线程的持有读锁次数】。如果 cachedHoldCounter 为null或者不是指向当前线程,那么利用 ThreadLocalHoldCounter 给当前线程弄一个 HoldCounter,然后赋值给 cachedHoldCounter,最后再对里面的 count 进行 ++ 操作;否则如果 cachedHoldCounter 指向的线程为当前线程并且加锁次数为0,则将cachedHoldCounter设置到 ThreadLocalHoldCounter里面,然后对里面的 count 进行 ++ 操作。ThreadLocalHoldCounter 底层是利用 ThreadLocal 保证线程安全,每个线程持有自己的 HoldeCounter。
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
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)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
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);
}
到这里,我们就可以知道,ReentrantReadWriteLock.ReadLock 底层是利用 ThreadLocalHoldCounter 来保存线程持有读锁的次数,而 ThreadLocalHoldCounter 是底层是利用 ThreadLocal 来保证线程安全。
2.1、关于 ThreadLocal 的思考延续
问题:
本来直接利用 ThreadLocalHoldCounter 来存每个线程的持有读锁的次数即可。为什么还会有【firstReader + firstReaderHoldCount】来保存首个获取读锁的线程和获取读锁得次数,还有 cachedHoldCounter 来存最后一个线程持有读锁的次数?
ThreadLocal 原理简讲:
其实底层就是利用 Thread 中的 threadLocals 属性,而这个属性的类型就是 ThreadLocal.ThreadLocalMap,它的底层是一个 Entry[]。
如何根据当前 ThreadLocal 定位到是存在 Entry[] 的slot呢?
根据当前 ThreadLocl 的 threadLocalHashCode 属性与 数据长度-1 进行 & 的位运算。
如何解决hash冲突?
它这里和HashMap不一样,它是利用线性探测来解决hash冲突的,即如果定位到的 slot 不是空闲的,则继续往后面一个格子一个格子去找。
ReentrentReadWriteLock 关于 ThreadLocal 的优化:
所以如果出现hash冲突的时候,不管是写入还是读取,效率都是非常的慢的。所以此处使用上面提到的两个点来存储持有读锁的次数,算是一种对读锁的优化,利用空间换时间,多存一份数据来避免当出现hash冲突时,读取和写入数据的性能差。
继续扩展:Netty 直接对 ThreadLocl 的优化:
其实在 netty 中,也是对 ThreadLocal 进行了一波优化,提供了 FastThreadLocal 和 FastThreadLocalThread。其中的优化点就是:
- FastThreadLocal 中的 InternalThreadLocalMap 的父类 UnpaddedInternalThreadLocalMap,它底层也是利用 Object[] 来存储 FastThreadLocal 的值,但是关于数据的 slot 定位,不是利用 hashCode 与数据长度进行 & 的位运算来定位的,而是本身存储了静态属性 nextIdex,底层是利用 AtomicInteger 来保证线程安全,并提供了获取nextIndex的静态方法静态方法给 FastThreadLocal 使用。
- FastThreadLocal 中有一个内部属性 index,在初始化 FastThreaLocal 实例时会利用上面提到的 InternalThreadLocalMap.nextVariableIndex() 方法进行初始化,来表示当前 FastThreadLocal 在 InternalThreadLocalMap 中 Object[]的 slot 的位置。
- 后续不管是读取还是写入,可利用 index 来直接定位 slot 来进行读写操作,性能必须快。
3、多线程释放锁如何准确释放自己的持有锁次数
通过上面,其实我们知道,ReentrantReadWriteLock 中的 ReadLock 是利用线程安全的 ThreadLocalHoldCounter 来统计成功持有读锁的次数,只不过对第一个线程和最后一个线程做一个优化而已。所以释放读锁对应的持有次数其实很简单。
- 首先判断 firstReader 是否指向自己,如果是的话,直接对 firstReaderHoldCount 进行 – 操作
- 否则判断 cachedHoldCounter 是否里面的线程id是否指向自己,如果是对里面的 count 进行 – 操作
- 最后,都不满足上面两点,就从 readHolds 属性中获取当前线程的 HoldCounter,即重 ThreadLocalHoldCounter 中获取。然后对里面的 count 进行 – 操作。
4、如何避免写锁被堵死
因为读写锁中,读写是互斥的,所以需要考虑一种场景,就是一直都有线程获取读锁,导致尝试获取写锁的线程被堵死,那么 ReentrantReadWriteLock 是如何避免的?
其实在上面的获取锁的代码里面的第二个if代码块已经给了我们答案。
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// ....
}
在上面的if代码块中,我们可以留意到 readerShouldBlock() 方法,它主要是判断当前获取读锁的线程是否需要阻塞住,即不允许去获取读锁。对于公平锁和非公平锁,它们的实现都是不一样的,我们分别看看。
4.1、NoFairSync#readerShouldBlock
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.
*/
return apparentlyFirstQueuedIsExclusive();
}
/**
* Returns {@code true} if the apparent first queued thread, if one
* exists, is waiting in exclusive mode. If this method returns
* {@code true}, and the current thread is attempting to acquire in
* shared mode (that is, this method is invoked from {@link
* #tryAcquireShared}) then it is guaranteed that the current thread
* is not the first queued thread. Used only as a heuristic in
* ReentrantReadWriteLock.
*/
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
其实代码的注释已经写得非常清楚了,此方法为了避免等待队列中存在其他等待获取写锁的线程,代码也很简单,就是判断等待队列的队头元素是否为shared等待状态,如果不是即是等待写锁,此时就会直接返回false了。
4.2、FairSync#readerShouldBlock
如果是公平锁就非常简单了,每次获取锁,不管是获取什么锁,都需要判断等待队列中是否有其他线程等待,如果存在,那么就需要在后面等待了。所以不存在等待死锁的线程被活活饿死的情况了。
下面我们也可以看看代码,也是非常的简单明了。
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
/**
* Queries whether any threads have been waiting to acquire longer
* than the current thread.
*
* <p>An invocation of this method is equivalent to (but may be
* more efficient than):
* <pre> {@code
* getFirstQueuedThread() != Thread.currentThread() &&
* hasQueuedThreads()}</pre>
*
* <p>Note that because cancellations due to interrupts and
* timeouts may occur at any time, a {@code true} return does not
* guarantee that some other thread will acquire before the current
* thread. Likewise, it is possible for another thread to win a
* race to enqueue after this method has returned {@code false},
* due to the queue being empty.
*
* <p>This method is designed to be used by a fair synchronizer to
* avoid <a href="AbstractQueuedSynchronizer#barging">barging</a>.
* Such a synchronizer's {@link #tryAcquire} method should return
* {@code false}, and its {@link #tryAcquireShared} method should
* return a negative value, if this method returns {@code true}
* (unless this is a reentrant acquire). For example, the {@code
* tryAcquire} method for a fair, reentrant, exclusive mode
* synchronizer might look like this:
*
* <pre> {@code
* protected boolean tryAcquire(int arg) {
* if (isHeldExclusively()) {
* // A reentrant acquire; increment hold count
* return true;
* } else if (hasQueuedPredecessors()) {
* return false;
* } else {
* // try to acquire normally
* }
* }}</pre>
*
* @return {@code true} if there is a queued thread preceding the
* current thread, and {@code false} if the current thread
* is at the head of the queue or the queue is empty
* @since 1.7
*/
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
本文详细解析了ReentrantReadWriteLock如何利用AQS的state记录读写锁状态,以及多线程持有读锁时的计数策略。通过firstReader和firstReaderHoldCount优化首个获取读锁的线程,使用ThreadLocalHoldCounter记录每个线程的持有读锁次数,避免ThreadLocal的hash冲突带来的性能影响。此外,还讨论了释放锁的准确性和防止写锁被堵死的机制,如NoFairSync和FairSync的readerShouldBlock方法。
1357

被折叠的 条评论
为什么被折叠?



