关于 ReentrantReadWriteLock 的延续思考

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。其中的优化点就是:

  1. FastThreadLocal 中的 InternalThreadLocalMap 的父类 UnpaddedInternalThreadLocalMap,它底层也是利用 Object[] 来存储 FastThreadLocal 的值,但是关于数据的 slot 定位,不是利用 hashCode 与数据长度进行 & 的位运算来定位的,而是本身存储了静态属性 nextIdex,底层是利用 AtomicInteger 来保证线程安全,并提供了获取nextIndex的静态方法静态方法给 FastThreadLocal 使用。
  2. FastThreadLocal 中有一个内部属性 index,在初始化 FastThreaLocal 实例时会利用上面提到的 InternalThreadLocalMap.nextVariableIndex() 方法进行初始化,来表示当前 FastThreadLocal 在 InternalThreadLocalMap 中 Object[]的 slot 的位置。
  3. 后续不管是读取还是写入,可利用 index 来直接定位 slot 来进行读写操作,性能必须快。

3、多线程释放锁如何准确释放自己的持有锁次数

通过上面,其实我们知道,ReentrantReadWriteLock 中的 ReadLock 是利用线程安全的 ThreadLocalHoldCounter 来统计成功持有读锁的次数,只不过对第一个线程和最后一个线程做一个优化而已。所以释放读锁对应的持有次数其实很简单。

  1. 首先判断 firstReader 是否指向自己,如果是的话,直接对 firstReaderHoldCount 进行 – 操作
  2. 否则判断 cachedHoldCounter 是否里面的线程id是否指向自己,如果是对里面的 count 进行 – 操作
  3. 最后,都不满足上面两点,就从 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());
}    
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值