JUC源码系列-ReentrantLock独占锁的释放

文章详细介绍了ReentrantLock的锁释放过程,包括tryRelease方法的实现,该方法减少持有锁的线程计数,当计数为0时释放锁。此外,还分析了unparkSuccessor方法,用于唤醒后继节点。整个释放锁的流程相对简单,涉及线程安全的状态更新和节点唤醒操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

开始之前先提一句, JAVA的内置锁在退出临界区之后是会自动释放锁的, 但是ReentrantLock这样的显式锁是需要自己显式的释放的, 所以在加锁之后一定不要忘记在finally块中进行显式的锁释放:

Lock lock = new ReentrantLock();
...
lock.lock();
try {
    // 更新对象
    //捕获异常
} finally {
    lock.unlock();
}

一定要记得在 finally 块中释放锁! ! !

Example: ReentrantLock的锁释放

由于锁的释放操作对于公平锁和非公平锁都是一样的, 所以, unlock的逻辑并没有放在 FairSync 或 NonfairSync 里面, 而是直接定义在 ReentrantLock类中:

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

由于释放锁的逻辑很简单, 这里就不画流程图了, 我们直接看源码:

release

release方法定义在AQS类中,描述了释放锁的流程

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

可以看出, 相比获取锁的acquire方法, 释放锁的过程要简单很多, 它只涉及到两个子函数的调用:

  • tryRelease(arg)

    该方法由继承AQS的子类实现, 为释放锁的具体逻辑

  • unparkSuccessor(h)

    唤醒后继线程

下面我们分别分析这两个子函数

tryRelease

tryRelease方法由ReentrantLock的静态类Sync实现:

多嘴提醒一下, 能执行到释放锁的线程, 一定是已经获取了锁的线程(这不废话嘛!)

另外, 相比获取锁的操作, 这里并没有使用任何CAS操作, 也是因为当前线程已经持有了锁, 所以可以直接安全的操作, 不会产生竞争.

protected final boolean tryRelease(int releases) {
    
    // 首先将当前持有锁的线程个数减1(回溯到调用源头sync.release(1)可知, releases的值为1)
    // 这里的操作主要是针对可重入锁的情况下, c可能大于1
    int c = getState() - releases; 
    
    // 释放锁的线程当前必须是持有锁的线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    
    // 如果c为0了, 说明锁已经完全释放了
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

是不是很简单? 代码都是自解释的

unparkSuccessor

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

锁成功释放之后, 接下来就是唤醒后继节点了, 这个方法同样定义在AQS中.

值得注意的是, 在成功释放锁之后(tryRelease 返回 true之后), 唤醒后继节点只是一个 “附加操作”, 无论该操作结果怎样, 最后 release操作都会返回 true.

事实上, unparkSuccessor 函数也不会返回任何值

接下来我们就看看unparkSuccessor的源码:

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    
    // 如果head节点的ws比0小, 则直接将它设为0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    // 通常情况下, 要唤醒的节点就是自己的后继节点
    // 如果后继节点存在且也在等待锁, 那就直接唤醒它
    // 但是有可能存在 后继节点取消等待锁 的情况
    // 此时从尾节点开始向前找起, 直到找到距离head节点最近的ws<=0的节点
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t; // 注意! 这里找到了之并有return, 而是继续向前找
    }
    // 如果找到了还在等待锁的节点,则唤醒它
    if (s != null)
        LockSupport.unpark(s.thread);
}

在上一篇文章分析 shouldParkAfterFailedAcquire 方法的时候, 我们重点提到了当前节点的前驱节点的 waitStatus 属性, 该属性决定了我们是否要挂起当前线程, 并且我们知道, 如果一个线程被挂起了, 它的前驱节点的 waitStatus值必然是Node.SIGNAL.

在唤醒后继节点的操作中, 我们也需要依赖于节点的waitStatus值.

下面我们仔细分析 unparkSuccessor函数:

首先, 传入该函数的参数node就是头节点head, 并且条件是

h != null && h.waitStatus != 0

h!=null 我们容易理解, h.waitStatus != 0是个什么意思呢?

我不妨逆向来思考一下, waitStatus在什么条件下等于0? 从上一篇文章到现在, 我们发现之前给 waitStatus赋值过的地方只有一处, 那就是shouldParkAfterFailedAcquire 函数中将前驱节点的 waitStatus设为Node.SIGNAL, 除此之外, 就没有了.

然而, 真的没有了吗???

其实还有一处, 那就是新建一个节点的时候, 在addWaiter 函数中, 当我们将一个新的节点添加进队列或者初始化空队列的时候, 都会新建节点 而新建的节点的waitStatus在没有赋值的情况下都会初始化为0.

所以当一个head节点的waitStatus为0说明什么呢, 说明这个head节点后面没有在挂起等待中的后继节点了(如果有的话, head的ws就会被后继节点设为Node.SIGNAL了), 自然也就不要执行 unparkSuccessor 操作了.

另外一个有趣的问题是, 为什么要从尾节点开始逆向查找, 而不是直接从head节点往后正向查找, 这样只要正向找到第一个, 不就可以停止查找了吗?

首先我们要看到,从后往前找是基于一定条件的:

if (s == null || s.waitStatus > 0)

即后继节点不存在,或者后继节点取消了排队,这一条件大多数条件下是不满足的。因为虽然后继节点取消排队很正常,但是通过上一篇我们介绍的shouldParkAfterFailedAcquire方法可知,节点在挂起前,都会给自己找一个waitStatus状态为SIGNAL的前驱节点,而跳过那些已经cancel掉的节点。

所以,这个从后往前找的目的其实是为了照顾刚刚加入到队列中的节点,这就牵涉到我们上一篇特别介绍的“尾分叉”了:

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode); //将当前线程包装成Node
    Node pred = tail;
    // 如果队列不为空, 则用CAS方式将当前节点设为尾节点
    if (pred != null) {
        node.prev = pred; //step 1, 设置前驱节点
        if (compareAndSetTail(pred, node)) { // step2, 将当前节点设置成新的尾节点
            pred.next = node; // step 3, 将前驱节点的next属性指向自己
            return node;
        }
    }
    enq(node); 
    return node;
}

如果你仔细看上面这段代码, 可以发现节点入队不是一个原子操作, 虽然用了compareAndSetTail操作保证了当前节点被设置成尾节点,但是只能保证,此时step1和step2是执行完成的,有可能在step3还没有来的及执行到的时候,我们的unparkSuccessor方法就开始执行了,此时pred.next的值还没有被设置成node,所以从前往后遍历的话是遍历不到尾节点的,但是因为尾节点此时已经设置完成,node.prev = pred操作也被执行过了,也就是说,如果从后往前遍历的话,新加的尾节点就可以遍历到了,并且可以通过它一直往前找。

所以总结来说,之所以从后往前遍历是因为,我们是处于多线程并发的条件下的,如果一个节点的next属性为null, 并不能保证它就是尾节点(可能是因为新加的尾节点还没来得及执行pred.next = node), 但是一个节点如果能入队, 则它的prev属性一定是有值的,所以反向查找一定是最精确的。

最后, 在调用了 LockSupport.unpark(s.thread) 也就是唤醒了线程之后, 会发生什么呢?

当然是回到最初的原点啦, 从哪里跌倒(被挂起)就从哪里站起来(唤醒)呗:

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // 喏, 就是在这里被挂起了, 唤醒之后就能继续往下执行了
    return Thread.interrupted();
}

那接下来做什么呢?

还记得我们上一篇在讲“锁的获取”的时候留的问题吗? 如果线程从这里唤醒了,它将接着往下执行。

注意,这里有两个线程:

一个是我们这篇讲的线程,它正在释放锁,并调用了LockSupport.unpark(s.thread) 唤醒了另外一个线程;

而这个另外一个线程,就是我们上一节讲的因为抢锁失败而被阻塞在LockSupport.park(this)处的线程。

我们再倒回上一篇结束的地方,看看这个被阻塞的线程被唤醒后,会发生什么。从上面的代码可以看出,他将调用 Thread.interrupted()并返回。

我们知道,Thread.interrupted()这个函数将返回当前正在执行的线程的中断状态,并清除它。接着,我们再返回到parkAndCheckInterrupt被调用的地方:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 我们在这里!在这里!!在这里!!!
            // 我们在这里!在这里!!在这里!!!
            // 我们在这里!在这里!!在这里!!!
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

具体来说,就是这个if语句

if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
    interrupted = true;

可见,如果Thread.interrupted()返回true,则 parkAndCheckInterrupt()就返回true, if条件成立,interrupted状态将设为true;

如果Thread.interrupted()返回false, 则 interrupted 仍为false。

再接下来我们又回到了for (;😉 死循环的开头,进行新一轮的抢锁。

假设这次我们抢到了,我们将从 return interrupted处返回,返回到哪里呢? 当然是acquireQueued的调用处啦:

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

我们看到,如果acquireQueued的返回值为true, 我们将执行 selfInterrupt():

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

而它的作用,就是中断当前线程。

绕了这么一大圈,到最后还是中断了当前线程,到底是在干嘛呢?

其实这一切的原因都在于:

我们并不知道线程被唤醒的原因。

具体来说,当我们从LockSupport.park(this)处被唤醒,我们并不知道是因为什么原因被唤醒,可能是因为别的线程释放了锁,调用了 LockSupport.unpark(s.thread),也有可能是因为当前线程在等待中被中断了,因此我们通过Thread.interrupted()方法检查了当前线程的中断标志,并将它记录下来,在我们最后返回acquire方法后,如果发现当前线程曾经被中断过,那我们就把当前线程再中断一次。

为什么要这么做呢?

从上面的代码中我们知道,即使线程在等待资源的过程中被中断唤醒,它还是会不依不饶的再抢锁,直到它抢到锁为止。也就是说,它是不响应这个中断的,仅仅是记录下自己被人中断过。

最后,当它抢到锁返回了,如果它发现自己曾经被中断过,它就再中断自己一次,将这个中断补上。

注意,中断对线程来说只是一个建议,一个线程被中断只是其中断状态被设为true, 线程可以选择忽略这个中断,中断一个线程并不会影响线程的执行。

线程中断是一个很重要的概念,这个我们以后有机会再细讲。(已成文,参见Thread类源码解读(3)——线程中断interrupt)

最后再小小的插一句,事实上在我们从return interrupted;处返回时并不是直接返回的,因为还有一个finally代码块:

finally {
    if (failed)
        cancelAcquire(node);
}

它做了一些善后工作,但是条件是failed为true,而从前面的分析中我们知道,要从for(;;)中跳出来,只有一种可能,那就是当前线程已经拿到了锁,因为整个争锁过程我们都是不响应中断的,所以不可能有异常抛出,既然是拿到了锁,failed就一定是true,所以这个finally块在这里实际上并没有什么用,它是为响应中断式的抢锁所服务的,这一点我们以后有机会再讲。

<think>嗯,用户之前让我介绍过Java多线程的基础知识,现在他们想深入了解JUC(java.util.concurrent)的源码。这说明用户可能已经掌握了多线程的基本概念和使用方法,现在想更深入理解底层实现,提升自己的技术水平。 首先,我需要确定用户对JUC的了解程度。他们可能已经用过一些工具类,比如ReentrantLock或CountDownLatch,但现在想了解这些类内部的机制。用户可能是中高级开发者,希望优化代码性能,或者准备面试,需要深入理解并发框架。 接下来,我需要考虑如何组织内容。直接深入源码可能会让用户感到困惑,所以应该先从JUC的整体结构入手,然后分模块解析核心类。比如,原子类、、同步器、并发集合、线程池等,每个部分都需要挑选关键点进行讲解。 用户可能需要知道关键设计模式,比如AQS(AbstractQueuedSynchronizer),因为这是JUC的核心。需要详细解释AQS的工作原理,如CLH队列、状态管理等。此外,CAS操作和volatile关键字也是实现无并发的基础,需要结合源码说明它们的作用。 另外,用户可能对常见类的实现感兴趣,比如ReentrantLock的公平与非公平区别,ConcurrentHashMap的分段或CAS优化,线程池的任务调度流程等。需要结合代码示例和流程图,帮助用户理解这些复杂机制。 同时,要注意避免过于冗长的代码展示,而是聚焦在关键方法和设计思路上。例如,AQS的acquire和release方法,以及它们如何被ReentrantLock或Semaphore调用。此外,可以比较不同版本的实现变化,比如JDK1.7和1.8中ConcurrentHashMap的改进,让用户了解优化方向。 最后,需要给出学习建议,推荐用户如何逐步深入源码,比如从简单的AtomicInteger开始,再逐步研究AQS和的实现。同时,提醒用户注意调试和测试的重要性,通过实际案例加深理解。 总结下来,回答的结构应该是先概述JUC的重要性,再分模块解析核心源码,结合关键设计模式和具体类实现,最后给出学习方法和资源推荐。这样既系统又深入,帮助用户由浅入深地掌握JUC源码。</think>好的!`java.util.concurrent`(JUC)是 Java 并发编程的核心工具包,包含大量高性能、线程安全的工具类和框架。深入源码理解其设计思想,能帮助你掌握高并发编程的精髓。以下从 **核心模块**、**设计模式**、**关键类源码分析** 三个维度进行详细解读。 --- ### **一、JUC 整体架构** JUC 的核心模块包括: 1. **原子类**(Atomic):基于 CAS 的无并发操作(如 `AtomicInteger`)。 2. **与同步器**(Locks):如 `ReentrantLock`、`AQS`(AbstractQueuedSynchronizer)。 3. **并发容器**(Collections):如 `ConcurrentHashMap`、`CopyOnWriteArrayList`。 4. **线程池**(Executors):`ThreadPoolExecutor`、`ForkJoinPool`。 5. **同步工具**(Tools):`CountDownLatch`、`CyclicBarrier`、`Semaphore`。 6. **并发流与 Future**:如 `CompletableFuture`、`ForkJoinTask`。 --- ### **二、核心源码解读** #### **1. AQS(AbstractQueuedSynchronizer)** AQS 是 JUC 和同步器的基石(如 `ReentrantLock`、`Semaphore` 均基于它实现)。 ##### **核心设计** - **状态变量(state)**:通过 `volatile int state` 表示资源状态(如的持有次数)。 - **CLH 队列**:一个双向链表实现的等待队列,存储竞争资源的线程。 - **模板方法模式**:子类只需实现 `tryAcquire`/`tryRelease`,AQS 负责线程排队和唤醒。 ##### **源码关键方法** ```java // 独占模式获取资源(如 ReentrantLock.lock()) public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } // 子类需实现 tryAcquire(如判断 state 是否为0) protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); } ``` ##### **实现示例:ReentrantLock 的公平** ```java // 公平的 tryAcquire 实现 protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && // 关键:检查队列是否有等待线程 compareAndSetState(0, acquires)) { // CAS 更新状态 setExclusiveOwnerThread(current); return true; } } // 处理重入逻辑(state 增加) else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; setState(nextc); return true; } return false; } ``` --- #### **2. 原子类(AtomicInteger)** 原子类的核心是 **CAS(Compare-And-Swap)** 操作,直接依赖 CPU 指令(如 x86 的 `cmpxchg`)。 ##### **源码关键实现** ```java public class AtomicInteger extends Number implements java.io.Serializable { private volatile int value; // 通过 volatile 保证可见性 public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } } // Unsafe 类的 CAS 操作(native 方法) public final class Unsafe { public final native boolean compareAndSwapInt( Object o, long offset, int expected, int x); } ``` ##### **CAS 的 ABA 问题** - **问题**:变量从 A 改为 B 又改回 A,CAS 无法感知中间变化。 - **解决方案**:使用 `AtomicStampedReference` 添加版本号。 --- #### **3. ConcurrentHashMap(JDK 1.8)** JDK 1.8 摒弃分段,改用 **CAS + synchronized** 实现更细粒度的。 ##### **关键设计** - **Node 数组(table)**:每个桶位(bucket)是一个链表或红黑树。 - **并发控制**: - **CAS 初始化数组**:避免多线程重复初始化。 - **synchronized 桶头节点**:仅定单个桶,减少竞争。 ##### **putVal 方法核心逻辑** ```java final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) tab = initTable(); // CAS 初始化数组 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value))) break; // CAS 插入新节点 } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); // 协助扩容 else { synchronized (f) { // 住桶头节点 // 插入链表或红黑树 } } } } ``` --- #### **4. ThreadPoolExecutor** 线程池的核心是 **任务队列** 和 **线程调度策略**。 ##### **核心参数** - **corePoolSize**:核心线程数(即使空闲也不销毁)。 - **workQueue**:任务队列(如 `LinkedBlockingQueue`)。 - **RejectedExecutionHandler**:拒绝策略(如丢弃、抛异常)。 ##### **execute 方法逻辑** ```java public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) // 创建新线程执行任务 return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { // 任务入队 // 二次检查线程池状态 } else if (!addWorker(command, false)) // 尝试创建非核心线程 reject(command); // 触发拒绝策略 } ``` --- ### **三、JUC 设计模式与技巧** 1. **CAS 无编程** - 通过 `Unsafe` 类直接操作内存,避免竞争(如原子类、ConcurrentHashMap)。 - 适用场景:读多写少,竞争不激烈。 2. **模板方法模式** - AQS 将公共逻辑(如线程排队)抽象到基类,子类只需实现特定方法(如 `tryAcquire`)。 3. **写时复制(Copy-On-Write)** - `CopyOnWriteArrayList` 在修改时复制整个数组,保证读操作无。 4. **分段优化** - JDK 1.7 的 `ConcurrentHashMap` 使用分段(Segment),减少粒度。 --- ### **四、高频源码面试题** 1. **AQS 的 CLH 队列为什么是双向链表?** - 答:为了支持取消操作(如超时或中断),需要快速定位前驱节点。 2. **ConcurrentHashMap 1.7 和 1.8 的区别?** - 1.7:分段 + ReentrantLock- 1.8:CAS + synchronized + 红黑树。 3. **线程池如何回收空闲线程?** - 答:`ThreadPoolExecutor` 通过 `getTask()` 中的 `poll(keepAliveTime)` 实现超时回收。 --- ### **五、学习建议** 1. **调试源码**:通过 IDE 调试 `ReentrantLock.lock()` 的执行流程。 2. **阅读注释**:JUC 类的源码注释通常包含设计思路和并发规则。 3. **手写简化版**:尝试实现一个简单的 AQS 或线程池。 理解 JUC 源码不仅能提升并发编程能力,还能深入掌握 Java 的高性能设计哲学。建议从 `AtomicInteger` 和 `ReentrantLock` 入手,逐步扩展到复杂类如 `ConcurrentHashMap` 和 `ThreadPoolExecutor`。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值