Java多线程(二)——ReentrantLock源码解析(补充1——从AQS中唤醒的线程)

文章详细阐述了ReentrantLock中线程被AQS唤醒后的执行流程,包括线程如何竞争锁,以及LockSupport.unpark()和thread.interrupt()如何影响线程。在竞争失败时,线程会再次进入等待状态。文章还探讨了线程在队列中的移动和锁的状态变化。

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

ReentrantLock源码解析(补充1)

上一章 ReentrantLock 源码解析 仅介绍了 ReentrantLock 的常用方法以及公平锁、非公平锁的实现。这里对上一章做一些补充。主要是:

1. AQS 中阻塞的线程被唤醒后的执行流程

线程尝试获取锁,不论公平锁还是非公平锁,如果获取不到,最后都会进入到 AQS 的队列中进行阻塞等待。直到持有锁的线程将锁释放,通过从后往前遍历查找 AQS 队列中距离 head 最近的可用节点,将其线程唤醒:LockSupport.unpark( s.thread)。 唤醒后的线程将会继续尝试竞争锁,我们分析一下它是如何竞争的:

1.1 线程被唤醒后,会继续执行 LockSupport.park() 之后的代码

LockSupport.park(thread) 方法会让Java线程进入 等待状态(WAITING),Java线程状态详情参见:

Java-线程基础

LockSupport.unpark(thread)调用之后,线程被唤醒,并获取到 CPU 时间片后,将继续运行 LockSupport.park() 位置后续的代码。(thread.interrupt()也可以将在 等待状态 线程唤醒)

在 ReentrantLock 中,AQS 中阻塞等待的线程被唤醒后,将继续下述代码的内容:

//AbstractQueuedSynchronized.java
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            //如果当前节点的前驱为head(这是一个空节点,标志着 AQS 双向链表中的头),那么可以尝试获取锁
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }

            //之前被阻塞在 parkAndCheckInterrupt() 方法中
            //shouldParkAfterFailedAcquire()方法将node的前驱节点中废弃节点(waitStatus = CANCELLED = 1)全都清理出队列。
            //1. 如果有废弃节点要清理,那么该方法return false,视图再次进入循环判断当前线程所在节点是否处在队列的队头位置。
            //2. 如果没有废弃节点要清理,那么说明当前线程不处在队头,那么就要进入 等待状态(WAITING) 阻塞。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

private final boolean parkAndCheckInterrupt() {
    //之前通过LockSupport.park()方法进入阻塞状态
    //LockSupport.unpark()或者interrupt()之后,会继续代码执行
    LockSupport.park(this);
    //Thread.interrupted()将会返回线程的打断标记,并且清空打断标记。
    return Thread.interrupted();
}

由于 LockSupport.park() 而处在 等待状态(WAITING) 的线程不仅可以通过 LockSupport.unpark() 方法唤醒,也可以通过 thread.interrupt() 方法唤醒,区别是后者将会让 thread 的打断标记置为 true。

线程唤醒后,继续执行 parkAndCheckInterrupt() 的 return 部分代码。而后进入到acquireQueued() 中继续死循环,尝试获取锁,或者重新回到 AQS 的等待队列中阻塞。

1.2 线程唤醒后的竞争分析

1. 解锁前:(node为双向指针,我画漏了)

假设thread1所在node之前所有的废弃节点(waitStatus = CANCELLED = 1)以及全部清空,当前 node(thread1) 已经处在队列头。

请添加图片描述

被唤醒时,进入 acquireQueued() 的 for(;😉 循环,由于 node(thread1) 为队头,由 acquireQueued() 代码可知,该线程将进行 tryAcquire() 尝试获取锁资源。

final boolean acquireQueued(final Node node, int arg) {
	...
    final Node p = node.predecessor();
    if (p == head && tryAcquire(arg)) {
        ...
    }
	...
}

如果没有出现竞争,如上一章讨论的上锁流程, node(thread1) 将会成为新的空head,thread1 从 node 中脱离出来,继续执行后续临界区代码。

本篇中,我们补充讨论出现竞争的情况:

2. 解锁后,出现竞争:(node为双向指针,我画漏了)

请添加图片描述

node(thread1) 被唤醒后,由于处在空head之后,为首个可用队头节点,将进入到 tryAcquire() 尝试获取所资源,同时 thread3 也在尝试获取锁资源。

3. 竞争失败 (node为双向指针,我画漏了)

请添加图片描述

如果 node(thread1) 竞争失败,将会进入 acquireQueue() 中下列部分,再次通过 parkAndCheckInterrupt() 的 LockSupport.lock() 方法阻塞起来。需要注意的是,这里没有额外动作,node(thread1) 在队列中的位置不变:

final boolean acquireQueued(final Node node, int arg) {
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
               ...
            }
            // tryAquire()竞争失败 return false后,进入下面部分
            if (shouldParkAfterFailedAcquire(p, node) &&
                //再次阻塞起来
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

4.竞争成功

请添加图片描述

竞争成功,thread1将会从node中解放出来,进入临界区运行后续代码:

Thread thread1 = new Thread(()->{
   lock.lock();
    //获取到锁之后,进入下面的临界区代码
   try{
       //临界区代码
   }finally{
       lock.unlock();
   }
});

而 node 将会被置空,并成为新的 空head 节点,原先的 空head 节点被抛弃:

final boolean acquireQueued(final Node node, int arg) {
    ...
    if (p == head && tryAcquire(arg)) {
        //争锁成功,成为新的 空head
        setHead(node);
        //将原先的空head抛弃
        p.next = null; // help GC
        failed = false;
        return interrupted;
    }
    ...     
}

private void setHead(Node node) {
    //成为新的head
    head = node;
    //将封装在 node 中的 thread 解放出去
    node.thread = null;
    //清空prev指向
    node.prev = null;
}

而竞争失败的 thread3 将会如上一篇所言,进入 AQS 等待队列,并通过 LockSupport.park() 进入 等待状态(WAITING)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值