当前位置:首页 > 技术 > 正文内容

ReentrantLock独占锁在AQS框架下的实现与运作

访客 技术 2026年9月19日 11

Java并发包中的ReentrantLock是一个可重入的独占锁,它依赖于抽象队列同步器(AbstractQueuedSynchronizer,简称AQS)框架来实现其核心功能。本文将深入探讨ReentrantLock(以非公平模式为例)的加锁与解锁机制,揭示其底层AQS的工作原理。

一、加锁(Lock)过程解析

当一个线程调用ReentrantLock实例的lock()方法试图获取锁时,实际上会委托给其内部的同步器Sync来处理。默认情况下,ReentrantLock使用非公平同步器NonfairSync。

1.1 首次尝试加锁

在非公平锁模式下,线程会直接尝试通过CAS操作抢占锁,而不会考虑AQS等待队列中是否存在其他线程。

final void lock() {
    // 立即尝试通过CAS将AQS的状态从0设置为1
    if (compareAndSetState(0, 1)) {
        // 如果成功,说明当前线程抢到了锁,并将其设为独占线程
        setExclusiveOwnerThread(Thread.currentThread());
    } else {
        // 如果首次尝试失败(锁已被占用),则进入AQS的通用获取流程
        acquire(1);
    }
}

如果compareAndSetState(0, 1)成功,表示当前没有任何线程持有锁,并且当前线程成功获取了锁,然后将当前线程设置为独占所有者线程,加锁过程结束。如果失败,说明锁已被其他线程持有,或者存在竞争,当前线程将进入acquire(1)方法,启动AQS的排队等待机制。

1.2 AQS通用获取流程:acquire(int units)

acquire(int units)是AQS框架中用于独占模式获取资源的模板方法。它包含了非公平尝试获取、构建等待节点、入队以及循环等待的逻辑。

public final void acquire(int units) {
    // 尝试获取锁,如果失败,则构建等待节点并入队,然后在队列中等待
    // 如果在等待过程中被中断,则设置中断标志
    if (!tryAcquire(units) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), units))
        Thread.currentThread().interrupt(); // 自我中断
}

这里有两个关键步骤:tryAcquire(units) 和 acquireQueued(addWaiter(Node.EXCLUSIVE), units)。

1.2.1 非公平模式下的tryAcquire(int units)

对于NonfairSync,其tryAcquire方法会调用nonfairTryAcquire。这是非公平锁的关键所在,它允许"插队"。

protected final boolean tryAcquire(int units) {
    return nonfairTryAcquire(units);
}

final boolean nonfairTryAcquire(int units) {
    final Thread current = Thread.currentThread();
    int currentLockState = getState(); // 获取当前AQS状态值(重入次数)

    if (currentLockState == 0) { // 锁当前处于空闲状态
        // 再次尝试CAS获取锁,这次是"插队"尝试,不检查队列
        if (compareAndSetState(0, units)) {
            setExclusiveOwnerThread(current); // 设置当前线程为锁的持有者
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) { // 当前线程已持有锁,进行重入
        int newLockState = currentLockState + units;
        if (newLockState < 0) { // 检查整数溢出
            throw new Error("Lock count exceeded maximum");
        }
        setState(newLockState); // 更新AQS状态,增加重入计数
        return true;
    }
    // 其他线程持有锁,或CAS失败
    return false;
}

如果nonfairTryAcquire成功获取锁(无论是首次CAS还是重入),则acquire方法结束。如果失败,则需要将当前线程封装成等待节点并加入AQS的等待队列。

1.2.2 构建等待节点并入队

addWaiter(Node.EXCLUSIVE)负责将当前线程封装成一个独占模式的Node节点,并尝试将其加入到AQS等待队列的尾部。

private Node addWaiter(Node.LockMode mode) { // LockMode 代替原来的 mode
    Node newNode = new Node(Thread.currentThread(), mode);
    Node currentTail = tail;

    // 快速路径:如果队列尾部不为空,尝试直接连接
    if (currentTail != null) {
        newNode.prev = currentTail;
        if (compareAndSetTail(currentTail, newNode)) {
            currentTail.next = newNode;
            return newNode;
        }
    }
    // 如果队列为空或快速路径失败,则进入enq循环确保入队
    return enqueue(newNode);
}

这里的enqueue(Node node)方法是一个自旋CAS操作,用于在等待队列为空时初始化队列(设置头节点和尾节点),或在并发入队时确保节点能被正确地添加到队列尾部。

private Node enqueue(final Node newNode) {
    for (;;) { // 自旋循环
        Node currentTail = tail;
        if (currentTail == null) { // AQS队列尚未初始化(头尾节点都为空)
            // 尝试通过CAS初始化头节点,这是一个哨兵节点
            if (compareAndSetHead(new Node())) {
                tail = head; // 将尾节点指向头节点
            }
        } else { // 队列已初始化,尝试将新节点连接到尾部
            newNode.prev = currentTail;
            if (compareAndSetTail(currentTail, newNode)) {
                currentTail.next = newNode;
                return currentTail; // 返回前一个尾节点
            }
        }
    }
}

1.2.3 循环等待与唤醒

acquireQueued(final Node node, int units)方法负责已入队的线程在队列中等待,并在满足条件时尝试获取锁或被唤醒。

final boolean acquireQueued(final Node node, int units) {
    boolean acquireFailed = true;
    try {
        boolean wasInterrupted = false;
        for (;;) { // 线程在此自旋等待,直到获取锁或被中断
            final Node predecessor = node.getPredecessor(); // 获取当前节点的前驱节点

            // 如果前驱节点是头节点(说明当前节点是队列中第一个真正等待的线程),
            // 则再次尝试获取锁(非公平的又一次尝试)
            if (predecessor == head && nonfairTryAcquire(units)) {
                setHead(node); // 成功获取锁,将当前节点设为新的头节点
                predecessor.next = null; // 解除原头节点的引用,利于GC
                acquireFailed = false;
                return wasInterrupted;
            }

            // 检查是否需要挂起当前线程
            // 如果前驱节点状态表明它可以安全地发出信号,且当前线程可以安全地挂起
            if (shouldParkCurrentThread(predecessor, node) &&
                parkAndCheckForInterrupt()) {
                wasInterrupted = true; // 如果在挂起期间被中断,设置中断标志
            }
        }
    } finally {
        // 如果获取锁失败(例如异常),则取消节点
        if (acquireFailed) {
            cancelAcquisition(node);
        }
    }
}

shouldParkCurrentThread(Node predecessor, Node currentNode):此方法决定当前线程是否可以安全地挂起。它主要处理前驱节点的waitStatus。

private static boolean shouldParkCurrentThread(Node predecessor, Node currentNode) {
    int predStatus = predecessor.waitStatus;
    if (predStatus == Node.SIGNAL) {
        // 前驱节点处于SIGNAL状态,表示它会在释放时唤醒当前节点,可以安全挂起
        return true;
    }
    if (predStatus > 0) { // predStatus > 0 意味着前驱节点已取消 (CANCELLED)
        // 跳过所有已取消的前驱节点
        do {
            currentNode.prev = predecessor = predecessor.prev;
        } while (predecessor.waitStatus > 0);
        predecessor.next = currentNode;
    } else {
        // predStatus 为 0 或 PROPAGATE。尝试将前驱节点状态设为SIGNAL
        // 以便它在释放时唤醒当前节点。返回false意味着当前线程还需要再次循环检查
        compareAndSetWaitStatus(predecessor, predStatus, Node.SIGNAL);
    }
    return false;
}

parkAndCheckForInterrupt():调用LockSupport.park(this)将当前线程挂起,直到被唤醒或中断。被唤醒后,返回线程是否被中断。

private final boolean parkAndCheckForInterrupt() {
    LockSupport.park(this); // 挂起当前线程
    return Thread.interrupted(); // 返回并清除中断状态
}

1.3 AQS等待节点(Node)状态

AQS内部使用Node类来表示等待队列中的线程。每个Node都有一个waitStatus字段,用于表示节点的状态。

static final class Node {
    static final Node SHARED_MODE_MARKER = new Node(); // 共享模式标记
    static final Node EXCLUSIVE_MODE_MARKER = null;   // 独占模式标记 (通常用null表示)

    static final int CANCELLED = 1;     // 节点已被取消
    static final int SIGNAL    = -1;    // 后继节点需要被唤醒
    static final int CONDITION = -2;    // 节点正在等待条件变量
    static final int PROPAGATE = -3;    // 共享模式下,释放后会无条件传播唤醒

    // ... 其他字段如 thread, prev, next ...
}

二、解锁(Unlock)过程解析

当锁的持有者线程调用ReentrantLock的unlock()方法时,会触发AQS的资源释放机制。

2.1 unlock() 方法

public void unlock() {
    sync.release(1); // 委托给同步器释放资源
}

2.2 AQS通用释放流程:release(int units)

release(int units)是AQS框架中用于独占模式释放资源的模板方法。

public final boolean release(int units) {
    // 尝试释放锁,如果释放成功(AQS状态变为0),则唤醒等待队列中的下一个线程
    if (tryRelease(units)) {
        Node currentHead = head;
        // 如果头节点不为空,且其waitStatus不为0(通常为SIGNAL,表示有后继需要唤醒)
        if (currentHead != null && currentHead.waitStatus != 0) {
            unparkSuccessor(currentHead); // 唤醒后继节点
        }
        return true;
    }
    return false;
}

2.2.1 尝试释放锁:tryRelease(int units)

tryRelease方法负责减少锁的重入计数,并在计数归零时释放锁的所有权。

protected final boolean tryRelease(int units) {
    // 计算释放后的新状态
    int newLockState = getState() - units;

    // 只有锁的持有者才能释放锁
    if (Thread.currentThread() != getExclusiveOwnerThread()) {
        throw new IllegalMonitorStateException();
    }

    boolean isLockFullyReleased = false;
    if (newLockState == 0) { // 如果新状态为0,表示锁完全释放
        isLockFullyReleased = true;
        setExclusiveOwnerThread(null); // 清除独占线程引用
    }
    setState(newLockState); // 更新AQS状态
    return isLockFullyReleased;
}

如果tryRelease返回true(即isLockFullyReleased为true),说明锁已完全释放(重入计数归零),此时需要唤醒等待队列中的下一个线程。

2.2.2 唤醒后续节点:unparkSuccessor(Node headNode)

此方法负责找到AQS等待队列中下一个需要被唤醒的有效节点,并使用LockSupport.unpark()将其唤醒。

private void unparkSuccessor(Node headNode) {
    // 获取头节点的waitStatus
    int headStatus = headNode.waitStatus;
    if (headStatus < 0) { // 如果状态是负数(如SIGNAL),尝试CAS将其设为0
        compareAndSetWaitStatus(headNode, headStatus, 0);
    }

    // 目标是唤醒头节点的直接后继节点
    Node successorNode = headNode.next;

    // 如果后继节点为空或者已被取消,则从队尾向前遍历查找一个有效的、未取消的节点
    if (successorNode == null || successorNode.waitStatus > 0) {
        successorNode = null; // 重置,准备查找
        for (Node current = tail; current != null && current != headNode; current = current.prev) {
            // 找到第一个waitStatus <= 0 的节点(即未取消的节点)
            if (current.waitStatus <= 0) {
                successorNode = current;
            }
        }
    }

    // 如果找到了有效的后继节点,则唤醒它
    if (successorNode != null) {
        LockSupport.unpark(successorNode.thread);
    }
}

被唤醒的线程会从其挂起的地方(parkAndCheckForInterrupt()之后)继续执行acquireQueued中的循环。它会再次检查自己是否是队列中的第一个等待线程,并尝试通过nonfairTryAcquire获取锁。如果成功,它将成为新的头节点,并继续其执行。如果失败,它将再次进入挂起状态,等待下一次被唤醒。

标签: ReentrantLock

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。