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;
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容