图解ReentrantReadWriteLock读写锁的实现原理(下)

简介: 图解ReentrantReadWriteLock读写锁的实现原理

tryAcquireShared()方法是一个模板方法,AQS类中定义语义,子类实现,如果返回1,表示获取锁成功,还有剩余资源,返回0表示获取成功,没有剩余资源,返回-1表示失败。

// 尝试以共享模式获取,返回1表示获取锁成功,还有剩余资源,返回0表示获取成功,没有剩余资源,返回-1,表示失败
protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    // exclusiveCount(c) 代表低 16 位, 写锁的 state,成立说明有线程持有写锁
    // 写锁的持有者不是当前线程,则获取读锁失败,【写锁允许降级】
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
        return -1;
    // 高 16 位,代表读锁的 state,共享锁分配出去的总次数
    int r = sharedCount(c);
    // 读锁是否应该阻塞
    if (!readerShouldBlock() && r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {   // 尝试增加读锁计数
        // 加锁成功
        // 加锁之前读锁为 0,说明当前线程是第一个读锁线程
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        // 第一个读锁线程是自己就发生了读锁重入
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            // cachedHoldCounter 设置为当前线程的 holdCounter 对象,即最后一个获取读锁的线程
            HoldCounter rh = cachedHoldCounter;
            // 说明还没设置 rh
            if (rh == null || rh.tid != getThreadId(current))
                // 获取当前线程的锁重入的对象,赋值给 cachedHoldCounter
                cachedHoldCounter = rh = readHolds.get();
            // 还没重入
            else if (rh.count == 0)
                readHolds.set(rh);
            // 重入 + 1
            rh.count++;
        }
        // 读锁加锁成功
        return 1;
    }
    // 逻辑到这 应该阻塞,或者 cas 加锁失败
    // 会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞
    return fullTryAcquireShared(current);
}
// 非公平锁 readerShouldBlock 偏向写锁一些,看 AQS 阻塞队列中第一个节点是否是写锁,是则阻塞,反之不阻塞
// 防止一直有读锁线程,导致写锁线程饥饿
// true 则该阻塞, false 则不阻塞
final boolean readerShouldBlock() {
    return apparentlyFirstQueuedIsExclusive();
}
// 下面是公平锁的readerShouldBlock
// 公平锁会检查 AQS 队列中是否有前驱节点, 没有(false)才去竞争
final boolean readerShouldBlock() {
    return hasQueuedPredecessors();
}
  • fullTryAcquireShared()方法是通过自旋的方式不断获取读锁,因为由于前面的readerShouldBlock返回false或者cas失败,导致没有获取到锁,需要不断重试。
final int fullTryAcquireShared(Thread current) {
    // 当前读锁线程持有的读锁次数对象
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        // 说明有线程持有写锁
        if (exclusiveCount(c) != 0) {
            // 写锁不是自己则获取锁失败
            if (getExclusiveOwnerThread() != current)
                return -1;
        } else if (readerShouldBlock()) {
            // 条件成立说明当前线程是 firstReader,当前锁是读忙碌状态,而且当前线程也是读锁重入
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                if (rh == null) {
                    // 最后一个读锁的 HoldCounter
                    rh = cachedHoldCounter;
                    // 说明当前线程也不是最后一个读锁
                    if (rh == null || rh.tid != getThreadId(current)) {
                        // 获取当前线程的 HoldCounter
                        rh = readHolds.get();
                        // 条件成立说明 HoldCounter 对象是上一步代码新建的
                        // 当前线程不是锁重入,在 readerShouldBlock() 返回 true 时需要去排队
                        if (rh.count == 0)
                            // 防止内存泄漏
                            readHolds.remove();
                    }
                }
                if (rh.count == 0)
                    return -1;
            }
        }
        // 越界判断
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 读锁加锁,条件内的逻辑与 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 != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}

doAcquireShared()是在获取读锁失败的时候加入AQS队列的逻辑。

private void doAcquireShared(int arg) {
    // 将当前线程关联到一个 Node 对象上, 模式为共享模式
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 获取前驱节点
            final Node p = node.predecessor();
            // 如果前驱节点就头节点就去尝试获取锁
            if (p == head) {
                // 再一次尝试获取读锁
                int r = tryAcquireShared(arg);
                // r >= 0 表示获取成功
                if (r >= 0) {
                    //【这里会设置自己为头节点,唤醒相连的后序的共享节点】
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 是否在获取读锁失败时阻塞                park 当前线程
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  • setHeadAndPropagate()方法是在后续读锁被唤醒后,抢到锁要处理的逻辑,包括修改队列的头结点,以及唤醒队列中的下一个共享节点。
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; 
    // 设置自己为 head 节点
    setHead(node);
    // propagate 表示有共享资源(例如共享读锁或信号量),为 0 就没有资源
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        // 获取下一个节点
        Node s = node.next;
        // 如果当前是最后一个节点,或者下一个节点是【等待共享读锁的节点】
        if (s == null || s.isShared())
            // 唤醒后继节点
            doReleaseShared();
    }
}


解锁原理


图解过程


由于上面t1线程加的写锁,所有其他的线程都被阻塞了,只有在t1线程解锁以后,其他线程才能被唤醒,我们现在看下t1线程被唤醒了,会发生什么?

  1. t1线程执行解锁w.unlock()成功,修改AQS中的state。

1671194965426.jpg

  • 这里的state变为了0_0。
  • AQS当前占用线程exclusiveOwnerThread属性变为null。
  1. t1线程唤醒队列中等待的老二, 为什么不是老大,因为老大是一个空节点,不会设置任何的线程。t2线程被唤醒后,抢锁成功,修改state中高16位为1。

1671194989280.jpg

  • 老二的线程节点变为蓝色节点
  • AQS中的state变为1_0。
  1. t2线程恢复运行,设置原来的老二节点为头节点

1671194995504.jpg

  1. t2线程要做的事情还没结束呢,因为是共享模式,它现在释放了,就此时也唤醒队列中的下一个共享节点。

1671195002504.jpg

  1. t3线程恢复去竞争读锁成功,这时state的高位+1,变成2。

1671195012529.jpg

  1. 这时候t3线程所在的Node设置为头节点,同时发现对列的下一个节点不是共享节点,而是独占节点,就不会唤醒后面的节点了。

1671195018851.jpg

  1. 之后t2线程和t3线程进入尾声,执行r.unlock操作,state的计数减一,直到变为0。

1671195025862.jpg

  1. 最后写锁线程t4被唤醒,去抢占锁成功,整个流程结束。

1671195031693.jpg

上面是整个解锁的流程,下面深入源码验证这个流程。


源码解析


  1. 写锁释放流程

WriteLock类的unlock()方法是入口方法,调用tryRelease()方法释放锁,如果成功,调用unparkSuccessor()方法唤醒线程。

public void unlock() {
    // 释放锁
    sync.release(1);
}
public final boolean release(int arg) {
    // 尝试释放锁
    if (tryRelease(arg)) {
        Node h = head;
        // 头节点不为空并且不是等待状态不是 0,唤醒后继的非取消节点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

tryRelease()方法是AQS提供的模板方法,返回true表示成功,false失败,由自定义同步器实现。

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    // 因为可重入的原因, 写锁计数为 0, 才算释放成功
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        // 设置占用线程为null
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}
  1. 读锁释放流程

ReadLock类的unlock()方法是释放共享锁的入口方法。

public void unlock() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

tryReleaseShared()方法是由AQS提供的模板方法,由自定义同步器实现。

protected final boolean tryReleaseShared(int unused) {
    //自选
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        // 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程,计数为 0 才是真正释放
        if (compareAndSetState(c, nextc))
            // 返回是否已经完全释放了 
            return nextc == 0;
    }
}

调用doReleaseShared()方法唤醒等待的线程,这个方法调用的地方有两处,还记得吗,一个这是里的解锁,还有一个是前面加共享锁阻塞的地方,唤醒后获取锁成功,也会调用doReleaseShared()方法。

private void doReleaseShared() {
    // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
  // 如果 head.waitStatus == 0 ==> Node.PROPAGATE
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // SIGNAL 唤醒后继
            if (ws == Node.SIGNAL) {
                // 因为读锁共享,如果其它线程也在释放读锁,那么需要将 waitStatus 先改为 0
              // 防止 unparkSuccessor 被多次执行
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;  
                // 唤醒后继节点
                unparkSuccessor(h);
            }
            // 如果已经是 0 了,改为 -3,用来解决传播性
            else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                
        }
        // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的 head,
        // 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点
        if (h == head)                   
            break;
    }
}


总结


本文讲解了读写锁ReentrantReadWriteLock的整个加锁、解锁的实现原理,并从源码的角度深入分析,希望对大家有帮助。

目录
相关文章
|
9月前
|
Java
【ReentrantReadWriteLock的实现原理】
【ReentrantReadWriteLock的实现原理】
|
11月前
|
安全
架构系列——通过ReentrantLock源码分析给对象上锁的原理
架构系列——通过ReentrantLock源码分析给对象上锁的原理
|
2月前
并发编程之读写锁ReadWriteLock的详细解析(带小案例)
并发编程之读写锁ReadWriteLock的详细解析(带小案例)
22 0
|
2月前
|
安全 Java 测试技术
ReentrantReadWriteLock(可重入读写锁)源码解读与使用
ReentrantReadWriteLock(可重入读写锁)源码解读与使用
|
2月前
|
安全 Java
ReentrantLock 原理你都知道吗?
通过以上步骤和示例代码,你应该对 ReentrantLock 的工作原理有了清晰的理解。欢迎关注威哥爱编程,一起学习成长。
|
12月前
读写锁原理解读(下)
读写锁原理解读(下)
读写锁原理解读(下)
|
8月前
图解ReentrantLock底层公平锁和非公平锁实现原理
图解ReentrantLock底层公平锁和非公平锁实现原理
87 0
|
12月前
|
Java 程序员 API
AQS 原理解读
AQS 原理解读
|
Java API
图解ReentrantReadWriteLock读写锁的实现原理(上)
图解ReentrantReadWriteLock读写锁的实现原理
165 0
图解ReentrantReadWriteLock读写锁的实现原理(上)

热门文章

最新文章