美文网首页
ReentrantLock 源码阅读

ReentrantLock 源码阅读

作者: 柯基去哪了 | 来源:发表于2021-04-27 17:13 被阅读0次

    一 API 阅读

    一种可重入的互斥锁。拥有和 synchronized 关键字相同的功能,除此之外,也有一定的功能扩展。

    一个 ReentrantLock 锁会被成功调用了 lock 方法,且还没有 unlock 的线程持有。检查一个线程是否持有锁的方法是 isHeldByCurrentThreadgetHoldCount

    构造函数可以包含一个可选的 boolean 值,表示构建的锁是一个 公平锁 还是
    非公平锁。使用默认的无参构造时,这个参数默认为 false 即非公平锁。当入参为 true 的时候,表示这是一个公平锁,排队的队列里等待最久的线程最先获得锁。传入参数为 false 的时候表示这是一个非公平锁,不会遵循公平锁里线程获取锁的策略。在竞争线程较多的情况下,使用公平锁会导致较低的吞入量

    需要注意的是,不定期地调用 tryLock 方法,会让争用线程不遵循公平锁的竞争模式。当恰巧锁资源被释放,而还有排队线程的时候,主动调用方法可能会成功提前获取到锁。

    使用 ReentrantLock 的常见惯例如下

    class X {
        private final ReentrantLock lock = new ReentrantLock();
        
        public void m() {
            lock.lock();
            try {
                // ... method body
            } finally {
                lock.unlock();
            }
        }
    }
    
    

    作为一个可重入锁,ReentrantLock 允许同一个线程的重入次数为 Integer.MAX_VALUE。

    二 部分代码阅读

    看这部分的代码的时候,需要结合前面的文章 AQS 部分一起来看。

    2.1 非公平锁的 lock 流程

        private final Sync sync;
    

    需要注意,这里这个成员变量 sync 是 reentrantLock 实现同步机制的核心类。因为 reentrantLock 使用的是 AQS 同步框架,而 sync 就是这个 AQS 的内部实现类。

    这里 sync 的实际实现,在 reentrantLock 里面分成了两大类。一个是公平锁实现,另一个是非公平锁实现。这里的编码 遵循了单一职责原则,也符合 AQS 同步器框架的推荐做法。

    当我们使用默认的无参构造函数创建一个 reentrantLock 实例。然后调用 lock() 方法,其流程如下:

    非公平锁.png

    实际调用的方法就是这里的

    java.util.concurrent.locks.ReentrantLock.NonfairSync#lock

        final void lock() {
            // cas 方式更新 AQS 的 state 成员值 +1
            if (compareAndSetState(0, 1))
                // 更新成功,设置独占锁线程引用为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // cas 更新失败,调用 AQS 的 acquire 方法
                acquire(1);
        }
    

    先尝试直接修改 AQS 内部维护的 state 成员变量,0 表示没有线程持有锁,由 CAS 方式更新为 1。如果更新成功,即表示当前线程成功持有了这个可重入独占锁,这时更新一下独占锁的线程引用为当前线程。

    如果 cas 方式更新 state 字段失败,那么就调用 AQS 内定义的 acquire 方法来尝试获取锁。这个方法之前在 AQS 源码阅读的时候详细读过。通过定义一套模板方法,来实现加锁操作。其中的方法

    • acquireQueued
    • addWaiter

    都是 AQS 自己实现,子类需要补充的方法是

    • tryAcquire

    在内部类

    java.util.concurrent.locks.ReentrantLock.NonfairSync

    中,这个方法的实现指向了

    java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    
    nonfairTryAcquire.png

    nonfairTryAcquire 代码如下:

        final boolean nonfairTryAcquire(int acquires) {
            // 获取当前线程
            final Thread current = Thread.currentThread();
            // 获取 AQS 内成员变量 state
            int c = getState();
            // 如果 state 为 0,表示锁空闲,尝试获取锁
            if (c == 0) {
                // cas 方式更新 state 字段
                if (compareAndSetState(0, acquires)) {
                    // 更新成功,设置当前线程引用为
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // state 不为 0,表示锁已经被某线程持有,先检查是不是自己持有
            else if (current == getExclusiveOwnerThread()) {
                // ReentrantLock 支持重入,所以累加 acquire 值
                int nextc = c + acquires;
                // 检查重入次数有没有溢出,溢出则抛出异常
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                // 未溢出,更新 state 值
                setState(nextc);
                return true;
            }
            // 获取锁失败,返回false
            return false;
        }
    
    todo nonfairTryAcquire 流程图
    

    如果此处的 nonfairTryAcquire 方法加锁失败,那么尝试加锁的线程会被加入同步队列排队(即 AQS 的 addWaiter 和 acquireQueued 方法)。而这个同步队列的排队唤醒线程机制又是默认的 非公平锁 机制。

    至此,我们应该知道的是,reentrantLock 的非公平锁核心机制是依赖于 AQS 的内容实现的。reentrantLock 本身也没有维护线程等待队列,这是 AQS 的工作。reentrantLock 只是通过内部类来实现了这个功能。

    2.2 公平锁的 lock 流程

    当以如下的方式声明一个 reentrantLock 对象时,我们就可以得到一个公平锁。

    ReentrantLock lock = new ReentrantLock(Boolean.TRUE);
    

    公平锁和非公平锁的区别在于:排队线程的获取锁时机是有顺序的,等待最久的线程最先获得锁

    与默认的 NoFairSync 实现相比,其他的都一样,主要的区别在自己实现的 tryAcquire 方法。

    java.util.concurrent.locks.ReentrantLock.FairSync#tryAcquire

        // 公平锁版本的 tryAcquire
        protected final boolean tryAcquire(int acquires) {
            // 获取当前线程
            final Thread current = Thread.currentThread();
            // 获取 AQS 同步器维护的锁状态字段 state
            int c = getState();
            // c == 0 表示当前锁处于空闲状态,可以尝试获取锁
            if (c == 0) {
                // hasQueuedPredecessors 方法用于判断当前尝试获取锁的线程是否需要排队,如果不需要排队则直接更新 state 字段并设置独占线程的引用,在判断体内返回 true
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 走到这里 c != 0,即锁已被占有,检查持有锁的线程是不是当前线程自己
            else if (current == getExclusiveOwnerThread()) {
                // 是当前线程持有锁,增加重入加锁次数,传入的 acquires 为 1
                int nextc = c + acquires;
                // 重入次数超过 Integer.MAX_VALUE 溢出为负数,抛出异常
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                // 未溢出,设置更新 state 字段值
                setState(nextc);
                // 返回 true
                return true;
            }
            // 尝试加锁失败,返回 false
            return false;
        }
    }
    

    公平锁与非公平锁的 tryAcquire 方法,主要区别在一个地方

    hasQueuedPredecessors

    当锁处于空闲状态时,公平锁加锁的前置判断条件多了这么一个方法。

    在 state = 0 的条件下,非公平锁内的线程不用检查 AQS 维护点队列信息而直接尝试争用锁;

        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());
        }
    

    拿到 AQS 维护的线程等待队列的头节点/尾节点引用。然后有一个嵌套的判断逻辑,返回 false 表示可以直接加锁,返回 true 的时候就需要入队。

    第一个条件 A,头节点不等于尾节点,即队列中还有在排队的线程。如果这个条件不满足(即头节点等于尾节点),说明队列中无排队线程,可以直接入队,不需要将现有线程入队。此时触发短路逻辑,直接返回 false。

    第二个条件组 B,两个条件满足一个即可

    1. 头节点的后继节点不为空
    2. 头节点的后继节点不是当前尝试获取锁的节点,如果这条为 false,表示排队里下一个即将拿到锁的线程就是当前线程

    在条件 A 返回 true 的情况下:

    当这两个判断 B1,B2 同时为 false,表示同步队列有排队线程,并且同步队列里排队最靠前都线程就是当前线程,这个时候也就 不需要排队, 直接获取。

    B1 返回 true,这个时候同步队列正处在初始化过程中,此时触发了条件组 B 的短路逻辑。整个条件组 B 返回 true。说明已经有其他线程在当前线程之前争用锁了,那么当前线程 需要排队 。整个判断逻辑返回 false。

    B1 返回 false,B2 返回 true。表示同步队列正在初始化过程中,并且排队等待的下一个线程不是当前线程,那当前线程依旧需要 加入排队队列 等候。

    2.3 unlock 流程

    公平锁和非公平锁的释放锁流程都是一样的。当我们调用

    reentrantLock.unlock()

    方法,debug 源代码,可以看到还是使用了实现了 AQS 内部类的成员变量的释放锁方法。

        public void unlock() {
            sync.release(1);
        }
    

    而对应的 release 方法的代码如下,这个模板方法依然是在 AQS 同步器内。

        public final boolean release(int arg) {
            // 尝试释放锁
            if (tryRelease(arg)) {
                // 获取头节点
                Node h = head;
                // 头节点不为空且头节点的节点状态不为0(不为0表示这个节点不是初始化虚拟节点)
                if (h != null && h.waitStatus != 0)
                    // 修改节点 status 字段并唤醒等待线程
                    unparkSuccessor(h);
                return true;
            }
            // 释放锁失败,返回 false
            return false;
        }
    

    tryRelease 方法和之前的 tryAcquire 方法一下,都是需要 AQS 同步器的实现类自己编写的部分。

    java.util.concurrent.locks.ReentrantLock.Sync#tryRelease

        // 内部类实现的 - 尝试释放锁方法,注意传入的 releases 值为 1
        protected final boolean tryRelease(int releases) {
            // 获取当前 state 值,然后减 1,得到一个释放锁之后 state 的期望值
            int c = getState() - releases;
            // 检查释放锁线程和加锁线程是不是同一个线程
            if (Thread.currentThread() != getExclusiveOwnerThread())
                // 不是的话,直接抛出异常
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 如果 state 期望值为 0,表示没有重入加锁,现在可以直接释放锁
            if (c == 0) {
                // 注意只有当 state 计数值为 0 的时候,才能释放锁,否则表示之前同一个线程有重入加锁操作
                free = true;
                // 取消独占线程的引用
                setExclusiveOwnerThread(null);
            }
            // 更新 state 值
            setState(c);
            // 返回释放锁标识位
            return free;
        }
    

    相关文章

      网友评论

          本文标题:ReentrantLock 源码阅读

          本文链接:https://www.haomeiwen.com/subject/knkprltx.html