ReentrantLock 源码阅读

一 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 源码阅读)