通过可重入锁ReentranLock弄懂AQS

部分文章来源:JavaGuide


什么是AQS

AQS的全称是抽象队列同步器

用来构建同步器

能简单且高效地构造出大量的锁和同步器


AQS的核心思想是什么

AQS 核心思想:

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态

如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制

这个机制 AQS 是用 CLH 队列锁 实现的,即将暂时获取不到锁的线程加入到队列中

AQS的目的:更好地处理资源的竞争


CLH队列锁(前置知识)

首先我们要弄懂AQS就要先有个前置知识,也就是什么是自旋锁,什么是CLH锁 

什么是自旋锁 

通过while来不断重试

因此自旋锁适用于锁竞争不激烈、锁持有时间短的场景

因为

1.插队问题:锁竞争激烈的情况下,可能存在一个线程一直被其他线程”插队“而一直获取不到锁的情况。

2.性能较差问题:实际的多处理上运行的自旋锁在锁竞争激烈时性能较差


什么是CLH锁

首先我们要知道CLH锁是对自旋锁的一个改进

我们的线程只需要轮询上一个队尾节点的状态变量

之前我们是中心化锁

1.为了解决插队问题:将线程组织成一个队列,保证先请求的线程先获得锁,避免了饥饿问题

2.锁状态去中心化,让每个线程在不同的状态变量中自旋,这样当一个线程释放它的锁时,只能使其后续线程的高速缓存失效,缩小了影响范围,从而减少了 CPU 的开销。


什么是中心化锁的问题

什么是中心化锁问题

所有线程都围绕一个全局锁竞争,如果有大量线程在竞争这个锁,那么会导致大量线程阻塞,降低系统的并发性能
缓存失效(Cache Coherency)问题:在多处理器系统中,每个处理器都有自己的缓存。当一个线程修改了锁的状态(比如锁被释放),所有其他处理器上的缓存数据会失效迫使这些处理器从主内存重新获取锁状态。这会导致频繁的缓存失效,增加系统的开销

在传统的中心化锁中,所有线程争抢的是同一把锁,导致线程都在同一个锁变量上竞争,发生激烈的争用。然而,去中心化锁通过让线程在不同的变量上自旋减小了竞争锁时的开销和缓存失效的影响
 


例子

我们会把线程加入到一个链表中
里面有一个节点,它来判断当前线程是否需要继续等待
这个线程不需要不断检查全局锁的状态,而是自旋等待它自己对应的节点状态变量
 


AQS原理详解

AQS核心思想

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态
这个机制是基于CLH锁实现的

CLH锁是对自旋锁的一种改进
 


AQS的核心原理图

我们有两个重要的东西

1.state资源变量

2.FIFO等待队列(CLH队列)

如何理解锁竞争:节点去争夺state变量,争夺成功就相当于抢到锁



AQS 使用 int 成员变量 state 表示同步状态
通过内置的FIFO 线程等待/等待队列来完成获取资源线程的排队工作

我们的state变量是用volatile变量修饰的,用于展示当前临界资源的获取锁的情况


且里面的三个方法getState(),setState(),compareAndSetState(),都是用了我们的final变量修饰
 



以可重入锁ReentranLock为例


state 的初始值为 0,表示锁处于未锁定状态
当线程 A 调用 lock() 方法时,会尝试通过 tryAcquire() 方法独占该锁,并让 state 的值加 1
如果成功了,那么线程 A 就获取到了锁
如果失败了,那么线程 A 就会被加入到一个等待队列(CLH 队列)中,直到其他线程释放该锁
假设线程 A 获取锁成功了,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加)

可重入性的体现:一个线程可以多次获取同一个锁而不会被阻塞。但是,这也意味着,一个线程必须释放与获取的次数相同的锁,才能让 state 的值回到 0,也就是让锁恢复到未锁定状态。只有这样,其他等待的线程才能有机会获取该锁

 


Node节点的waitStatus

如果 waitStatus > 0 ,表明节点的状态已经取消等待获取资源

如果 waitStatus < 0 ,表明节点的处于有效的等待状态

因此在 AQS 的源码中,经常使用 > 0< 0 来对 waitStatus 进行判断


自定义同步器

我们的AQS的实现类都继承AbstractQueuedSynchronizer 并重写指定的方法

例如CountDownLatch和ReentranLock,我们都继承了这个类然后重写指定的方法

AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的钩子方法


AQS资源共享方式

AQS的两种资源共享方式

AQS 定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock

Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

一般来说,自定义同步器的共享方式要么是独占,要么是共享,他们也只需实现tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一种即可

但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock


什么是独占方式和共享方式

独占方式(Exclusive Mode)
  • 定义同一时间只有一个线程能获取资源,其他线程必须等待。
  • 实现:通过tryAcquiretryRelease方法实现资源的获取和释放。
  • 应用场景:适用于互斥锁(如ReentrantLock),确保资源不会被多个线程同时访问。
共享方式(Shared Mode)
  • 定义多个线程可以同时获取资源,适用于允许多个线程并发访问的场景。
  • 实现:通过tryAcquireSharedtryReleaseShared方法实现资源的获取和释放。
  • 应用场景:适用于信号量(如Semaphore)和读写锁(如ReentrantReadWriteLock),允许多个线程同时读取资源。
区别
  • 独占方式:资源一次只能被一个线程持有。
  • 共享方式:资源可以被多个线程同时持有。
示例
  • 独占方式ReentrantLock确保同一时间只有一个线程持有锁。
  • 共享方式Semaphore允许多个线程同时获取许可。
总结
  • 独占方式:互斥访问,适用于需要排他性控制的场景。
  • 共享方式:并发访问,适用于允许多个线程同时操作的场景

 


AQS资源获取源码分析(独占模式)

AQS 中以独占模式获取资源的入口方法是 acquire() ,如下:

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

acquire()获取锁流程

1.在 acquire() 中,线程会先尝试获取共享资源

2.如果获取失败,会将线程封装为 Node 节点加入到 AQS 的等待队列中

3.加入队列之后,会让等待队列中的线程尝试获取资源,并且会对线程进行阻塞操作


三个方法

tryAcquire() :尝试获取锁(模板方法),AQS 不提供具体实现,由子类实现。

addWaiter() :如果获取锁失败,会将当前线程封装为 Node 节点加入到 AQS 的 CLH 变体队列中等待获取锁。

acquireQueued() :对线程进行阻塞、唤醒,并调用 tryAcquire() 方法让队列中的线程尝试获取锁


tryAcquire()分析

AQS 中对应的 tryAcquire() 模板方法
// AQS
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

tryAcquire() 方法是 AQS 提供的模板方法,不提供默认实现

ReentranLock举例子

这里分析 tryAcquire() 方法时,以 ReentrantLock 的非公平锁(独占锁)为例进行分析

ReentrantLock 内部实现的 tryAcquire() 会调用到下边的 nonfairTryAcquire()

// ReentrantLock
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 1、获取 AQS 中的 state 状态
    int c = getState();
    // 2、如果 state 为 0,证明锁没有被其他线程占用
    if (c == 0) {
        // 2.1、通过 CAS 对 state 进行更新
        if (compareAndSetState(0, acquires)) {
            // 2.2、如果 CAS 更新成功,就将锁的持有者设置为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 3、如果当前线程和锁的持有线程相同,说明发生了「锁的重入」
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 3.1、将锁的重入次数加 1
        setState(nextc);
        return true;
    }
    // 4、如果锁被其他线程占用,就返回 false,表示获取锁失败
    return false;
}

nonfairTryAcquire() 方法内部,主要通过两个核心操作去完成资源的获取:

  • 通过 CAS 更新 state 变量state == 0 表示资源没有被占用。state > 0 表示资源被占用,此时 state 表示重入次数。
  • 通过 setExclusiveOwnerThread() 设置持有资源的线程。

如果线程更新 state 变量成功,就表明获取到了资源

因此将持有资源的线程设置为当前线程即可


addWaiter()分析

在通过 tryAcquire() 方法尝试获取资源失败之后

会调用 addWaiter() 方法将当前线程封装为 Node 节点加入 AQS 内部的队列中

addWaite() 代码如下:

// AQS
private Node addWaiter(Node mode) {
    // 1、将当前线程封装为 Node 节点。
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    // 2、如果 pred != null,则证明 tail 节点已经被初始化,直接将 Node 节点加入队列即可。
    if (pred != null) {
        node.prev = pred;
        // 2.1、通过 CAS 控制并发安全。
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 3、初始化队列,并将新创建的 Node 节点加入队列。
    enq(node);
    return node;
}
节点入队的并发安全

addWaiter() 方法中,需要执行 Node 节点 入队 的操作。由于是在多线程环境下

因此需要通过 CAS 操作保证并发安全

通过 CAS 操作去更新 tail 指针指向新入队的 Node 节点,CAS 可以保证只有一个线程会成功修改 tail 指针,以此来保证 Node 节点入队时的并发安全

AQS内部队列的初始化

在执行 addWaiter() 时,如果发现 pred == null ,即 tail 指针为 null

则证明队列没有初始化

需要调用 enq() 方法初始化队列,并将 Node 节点加入到初始化后的队列中

代码如下:

// AQS
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) {
            // 1、通过 CAS 操作保证队列初始化的并发安全
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 2、与 addWaiter() 方法中节点入队的操作相同
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

enq() 方法中初始化队列,在初始化过程中,也需要通过 CAS 来保证并发安全

初始化队列总共包含两个步骤:

1.初始化 head 节点

2.tail 指向 head 节点


acquireQueued()分析

AQS 中以独占模式获取资源的入口方法是 acquire() ,如下:

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

acquire() 方法中,通过 addWaiter() 方法将 Node 节点加入队列之后,就会调用 acquireQueued() 方法。

代码如下:

// AQS:令队列中的节点尝试获取锁,并且对线程进行阻塞。
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 1、尝试获取锁。
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 2、判断线程是否可以阻塞,如果可以,则阻塞当前线程。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 3、如果获取锁失败,就会取消获取锁,将节点状态更新为 CANCELLED。
        if (failed)
            cancelAcquire(node);
    }
}

acquireQueued() 方法中,主要做两件事情:

  • 尝试获取资源: 当前线程加入队列之后,如果发现前继节点是 head 节点,说明当前线程是队列中第一个等待的节点,于是调用 tryAcquire() 尝试获取资源。
  • 阻塞当前线程 :如果尝试获取资源失败,就需要阻塞当前线程,等待被唤醒之后获取资源

1.尝试获取资源

acquireQueued() 方法中,尝试获取资源总共有 2 个步骤:

  • p == head :表明当前节点的前继节点为 head 节点。此时当前节点为 AQS 队列中的第一个等待节点。
  • tryAcquire(arg) == true :表明当前线程尝试获取资源成功

在成功获取资源之后,就需要将当前线程的节点 从等待队列中移除

移除操作为:将当前等待的线程节点设置为 head 节点(head 节点是虚拟节点,并不参与排队获取资源)


2、阻塞当前线程

AQS 中,当前节点的唤醒需要依赖于上一个节点

如果上一个节点取消获取锁,它的状态就会变为 CANCELLEDCANCELLED 状态的节点没有获取到锁,也就无法执行解锁操作对当前节点进行唤醒。

因此在阻塞当前线程之前,需要跳过 CANCELLED 状态的节点

通过 shouldParkAfterFailedAcquire() 方法来判断当前线程节点是否可以阻塞,如下:

// AQS:判断当前线程节点是否可以阻塞。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 1、前继节点状态正常,直接返回 true 即可。
    if (ws == Node.SIGNAL)
        return true;
    // 2、ws > 0 表示前继节点的状态异常,即为 CANCELLED 状态,需要跳过异常状态的节点。
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 3、如果前继节点的状态不是 SIGNAL,也不是 CANCELLED,就将状态设置为 SIGNAL。
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

shouldParkAfterFailedAcquire() 方法中的判断逻辑:

  • 如果发现前继节点的状态是 SIGNAL ,则可以阻塞当前线程。
  • 如果发现前继节点的状态是 CANCELLED ,则需要跳过 CANCELLED 状态的节点。
  • 如果发现前继节点的状态不是 SIGNALCANCELLED ,表明前继节点的状态处于正常等待资源的状态,因此将前继节点的状态设置为 SIGNAL ,表明该前继节点需要对后续节点进行唤醒

当判断当前线程可以阻塞之后,通过调用 parkAndCheckInterrupt() 方法来阻塞当前线程

内部使用了 LockSupport 来实现阻塞。LockSupoprt 底层是基于 Unsafe 类来阻塞线程,代码如下:

// AQS
private final boolean parkAndCheckInterrupt() {
    // 1、线程阻塞到这里
    LockSupport.park(this);
    // 2、线程被唤醒之后,返回线程中断状态
    return Thread.interrupted();
}

为什么在线程被唤醒之后,要返回线程的中断状态呢?

parkAndCheckInterrupt() 方法中,当执行完 LockSupport.park(this) ,线程会被阻塞,代码如下:

// AQS
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    // 线程被唤醒之后,需要返回线程中断状态
    return Thread.interrupted();
}

当线程被唤醒之后,需要执行 Thread.interrupted() 来返回线程的中断状态,这是为什么呢?

这个和线程的中断协作机制有关系,线程被唤醒之后,并不确定是被中断唤醒,还是被 LockSupport.unpark() 唤醒,因此需要通过线程的中断状态来判断

acquire() 方法中,为什么需要调用 selfInterrupt()

acquire() 方法代码如下:

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

acquire() 方法中,当 if 语句的条件返回 true 后,就会调用 selfInterrupt() ,该方法会中断当前线程,为什么需要中断当前线程呢?

if 判断为 true 时,需要 tryAcquire() 返回 false ,并且 acquireQueued() 返回 true

其中 acquireQueued() 方法返回的是线程被唤醒之后的 中断状态 ,通过执行 Thread.interrupted() 来返回。该方法在返回中断状态的同时,会清除线程的中断状态。

因此如果 if 判断为 true ,表明线程的中断状态为 true ,但是调用 Thread.interrupted() 之后,线程的中断状态被清除为 false

因此需要重新执行 selfInterrupt() 来重新设置线程的中断状态


AQS资源释放源码分析(独占模式)

release()代码

AQS 中以独占模式释放资源的入口方法是 release() ,代码如下:

// AQS
public final boolean release(int arg) {
    // 1、尝试释放锁
    if (tryRelease(arg)) {
        Node h = head;
        // 2、唤醒后继节点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

release() 方法中,主要做两件事:尝试释放锁和唤醒后继节点


1.尝试释放锁:tryRelease()

通过 tryRelease() 方法尝试释放锁,该方法为模板方法,由自定义同步器实现,因此这里仍然以 ReentrantLock 为例来讲解。

ReentrantLock 中实现的 tryRelease() 方法如下:

重点在于我们的free变量

// ReentrantLock
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 1、判断持有锁的线程是否为当前线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 2、如果 state 为 0,则表明当前线程已经没有重入次数。因此将 free 更新为 true,表明该线程会释放锁。
    if (c == 0) {
        free = true;
        // 3、更新持有资源的线程为 null
        setExclusiveOwnerThread(null);
    }
    // 4、更新 state 值
    setState(c);
    return free;
}

tryRelease() 方法中,会先计算释放锁之后的 state 值,判断 state 值是否为 0。

  • 如果 state == 0 ,表明该线程没有重入次数了,更新 free = true 并修改持有资源的线程为 null,表明该线程完全释放这把锁
  • 如果 state != 0 ,表明该线程还存在重入次数,因此不更新 free 值,free 值为 false 表明该线程没有完全释放这把锁。

之后更新 state 值,并返回 free 值,free 值表明线程是否完全释放锁


2.唤醒后继节点:unparkSuccessor()

如果 tryRelease() 返回 true ,也就是我们的返回的free值为true表明线程已经没有重入次数了,锁已经被完全释放,因此需要唤醒后继节点

在唤醒后继节点之前,需要判断是否可以唤醒后继节点

分析判断条件

判断条件为: h != null && h.waitStatus != 0 。这里解释一下为什么要这样判断:

  • h == null :表明 head 节点还没有被初始化,也就是 AQS 中的队列没有被初始化,因此无法唤醒队列中的线程节点。
  • h != null && h.waitStatus == 0 :表明头节点刚刚初始化完毕(节点的初始化状态为 0),后继节点线程还没有成功入队,因此不需要对后续节点进行唤醒。(当后继节点入队之后,会将前继节点的状态修改为 SIGNAL ,表明需要对后继节点进行唤醒)
  • h != null && h.waitStatus != 0 :其中 waitStatus 有可能大于 0,也有可能小于 0。其中 > 0 表明节点已经取消等待获取资源,< 0 表明节点处于正常等待状态
具体如何唤醒后续节点

接下来进入 unparkSuccessor() 方法查看如何唤醒后继节点:

// AQS:这里的入参 node 为队列的头节点(虚拟头节点)
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    // 1、将头节点的状态进行清除,为后续的唤醒做准备。
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    // 2、如果后继节点异常,则需要从 tail 向前遍历,找到正常状态的节点进行唤醒。
    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;
    }
    if (s != null)
        // 3、唤醒后继节点
        LockSupport.unpark(s.thread);
}

unparkSuccessor() 中,如果头节点的状态 < 0 (在正常情况下,只要有后继节点,头节点的状态应该为 SIGNAL ,即 -1),表示需要对后继节点进行唤醒

因此这里提前清除头节点的状态标识,将状态修改为 0,表示已经执行了对后续节点唤醒的操作

如果 s == null 或者 s.waitStatus > 0 ,表明后继节点异常,此时不能唤醒异常节点,而是要找到正常状态的节点进行唤醒

因此需要从 tail 指针向前遍历,来找到第一个状态正常(waitStatus <= 0)的节点进行唤醒

为什么要从 tail 指针向前遍历,而不是从 head 指针向后遍历,寻找正常状态的节点呢?

遍历的方向和 节点的入队操作 有关

入队方法如下:

// AQS:节点入队方法
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        // 1、先修改 prev 指针。
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            // 2、再修改 next 指针。
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

addWaiter() 方法中,node 节点入队需要修改 node.prevpred.next 两个指针,但是这两个操作并不是 原子操作 ,先修改了 node.prev 指针,之后才修改 pred.next 指针。

在极端情况下,可能会出现 head 节点的下一个节点状态为 CANCELLED

此时新入队的节点仅更新了 node.prev 指针,还未更新 pred.next 指针,如下图:

这样如果从 head 指针向后遍历,无法找到新入队的节点,因此需要从 tail 指针向前遍历找到新入队的节点


图解AQS工作原理

由于 AQS 是底层同步工具,获取和释放资源的方法并没有提供具体实现,因此这里基于 ReentrantLock 来画图进行讲解。

假设总共有 3 个线程尝试获取锁,线程分别为 T1T2T3


T1获取锁成功:初始化AQS队列

此时,假设线程 T1 先获取到锁,线程 T2 排队等待获取锁在线程 T2 进入队列之前,需要对 AQS 内部队列进行初始化。head 节点在初始化后状态为 0

AQS 内部初始化后的队列如下图:


T2尝试获取锁:T2入AQS队列

此时,线程 T2 尝试获取锁。由于线程 T1 持有锁,因此线程 T2 会进入队列中等待获取锁

同时会将前继节点( head 节点)的状态由 0 更新为 SIGNAL ,表示需要对 head 节点的后继节点进行唤醒

AQS 内部队列如下图所示:


T3尝试获取锁:T3入AQS队列

此时,线程 T3 尝试获取锁。由于线程 T1 持有锁,因此线程 T3 会进入队列中等待获取锁

同时会将前继节点(线程 T2 节点)的状态由 0 更新为 SIGNAL ,表示线程 T2 节点需要对后继节点进行唤醒

AQS 内部队列如下图所示:


T1释放锁:唤醒后续节点T2

此时,假设线程 T1 释放锁,会唤醒后继节点 T2 。线程 T2 被唤醒后获取到锁,并且会从等待队列中退出。

这里线程 T2 节点退出等待队列并不是直接从队列移除

而是令线程 T2 节点成为新的 head 节点,以此来退出资源获取的等待

此时 AQS 内部队列如下所示:


T2释放锁:唤醒后续节点T3

此时,假设线程 T2 释放锁,会唤醒后继节点 T3 。线程 T3 获取到锁之后,同样也退出等待队列,即将线程 T3 节点变为 head 节点来退出资源获取的等待

此时 AQS 内部队列如下所示:


通过ReentranLock简单总结AQS 

理解AQS的两个重要东西

1.state资源变量

2.FIFO等待队列(CLH队列)

如何理解锁竞争:节点去争夺state变量,争夺成功就相当于抢到锁


AQS 使用 int 成员变量 state 表示同步状态
通过内置的FIFO 线程等待/等待队列来完成获取资源线程的排队工作
我们的state变量是用volatile变量修饰的,用于展示当前临界资源的获取锁的情况


AQS模板

AQS定义了一系列的模板方法,我们子类重写它的模板方法

例如CountDownLatch和ReentranLock

所以我们自定义队列同步器的时候,也要重写部分AQS规定的方法


资源获取流程简单小结

初始化时,head节点就是当前拿到锁的节点

初始化后,head节点就是上次拿到锁的节点,拿到锁的节点执行完后他再去唤醒后面的线程

争夺state

获取state变量,如果state=0说明资源没被抢,我们cas来抢state

如果线程不为0,我们看看当前线程是否是当前持有state资源的线程,如果是那就是重入了,我们state➕➕

争夺失败的节点入队

没抢到state的节点入队,开始前检查通过pre是否为null来判断是否初始化

没初始化则用cas操作初始化队列,否则则用cas操作入队列

我们只有发现前一个节点是head的时候我们才可以获取锁,否则就阻塞,然后等待被唤醒

ps:阻塞线程前要跳过cancelled节点

如果获取锁时出现异常失败就会被取消,更新节点状态为cancelled

获取资源后移除当前线程的节点

在成功获取资源之后,就需要将当前线程的节点 从等待队列中移除

移除操作为:将当前等待的线程节点设置为 head 节点(head 节点是虚拟节点,并不参与排队获取资源)


资源释放流程简单小结

我们分为两个步骤,释放锁和唤醒后续节点

释放锁

我们有个boolean类型的free变量表示资源是否释放

释放资源,state减少

检查持有锁的是否为当前线程

如果state不为0,就是还是持有锁状态【例如可重入锁】,我们更新state

如果state为0说明当前线程已经没有可重入次数,我们把持有锁的线程设置为null【说明资源释放,目前没人持有资源】,我们更新state

判断是否可以唤醒后续节点

根据head节点和waitStatus判断

如果是刚初始化【也就是当前节点是head】或者没初始化,就不用找后续节点

如果waitstatus=0,也就是当前节点不是正常等待或取消状态,就不用找后续节点

唤醒后续节点

如果头节点状态为Signal即为-1,表示需要对后续节点进行唤醒

清空头节点状态

通过tail指针从前往后遍历找到第一个正常的节点进行唤醒


CAS操作

修改state,队列入队, 初始化队列都是通过CAS操作来保证我们的并发安全

队列入队用CAS产生的问题

node 节点入队需要修改 node.prevpred.next 两个指针,但是这两个操作并不是 原子操作 ,先修改了 node.prev 指针,之后才修改 pred.next 指针。

错误情况:会导致新入队的节点仅更新了 node.prev 指针,还未更新 pred.next 指针

如下图:

这就是为什么我们我们从尾节点tail开始,从后往前遍历找到正常的节点来唤醒


面试时AQS的回答引导 

AQS是什么:抽象队列同步器

AQS的作用是啥:它是Java中大量的锁和同步器的基类,用来构建锁和同步器例如CountDownLatch倒计时器和ReentranLock可重入锁

通过ReentranLock举例子来说明AQS的原理

首先,先说明什么是自旋锁->自旋锁的不公平问题,再介绍到CLH锁

CLH锁是如何优化和解决自旋锁的问题的不公平问题和中心化锁问题以及什么是中心化锁问题

开始介绍AQS里面的东西,也就是我们的CLH队列和State状态变量

然后用ReentranLock的锁争夺和释放例子,看看节点是怎么入队的,资源state是怎么被争夺的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/968492.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【编程实践】vscode+pyside6环境部署

1 PySide6简介 PySide6是Qt for Python的官方版本&#xff0c;支持Qt6&#xff0c;提供Python访问Qt框架的接口。优点包括官方支持、LGPL许可&#xff0c;便于商业应用&#xff0c;与Qt6同步更新&#xff0c;支持最新特性。缺点是相比PyQt5&#xff0c;社区资源较少。未来发展…

DeepSeek-R1本地搭建

1. 前言 现在deepseek火上天了&#xff0c;因为各种应用场景,加上DeepSeek一直网络异常&#xff0c;所以本地部署Deepseek成为大家的另一种选择。 目前网络上面关于DeepSeek的部署方式有很多&#xff0c;但是太麻烦了&#xff0c;本文是一篇极为简单的DeepSeek本地部署方式&…

《qt open3d网格拉普拉斯平滑》

qt open3d网格拉普拉斯平滑 效果展示二、流程三、代码效果展示 二、流程 创建动作,链接到槽函数,并把动作放置菜单栏 参照前文 三、代码 1、槽函数实现 void on_actionFilterLaplacian_triggered();void MainWindow::on_actionFil

DeepSeek-V3网络模型架构图解

DeepSeek-V3网络架构的创新主要在两次&#xff0c;分别是在前馈层的MOE&#xff08;混合专家模型&#xff09;和在注意力中的MHA&#xff08;多头潜在注意力&#xff0c;一种注意力计算规模压缩技术&#xff09;。 MOE&#xff08;混合专家模型&#xff09; 回顾最初的MOE GS…

.net6 mvc 获取网站(服务器端)的IP地址和端口号

注意&#xff1a;是网站的&#xff0c;服务端的 IP地址&#xff0c; 不是当前用户电脑的、本地的IP地址 两个图&#xff1a; 分析&#xff1a; var AbsolutePath HttpContext.Request.Url.AbsolutePath;//"/Meeting/GetLastMeetingOL"var AbsoluteUri HttpContext.…

无人机遥感图像拼接及处理实践技术:生态环境监测、农业、林业等领域,结合图像拼接与处理技术,能够帮助我们更高效地进行地表空间要素的动态监测与分析

近年来&#xff0c;无人机技术在遥感领域的应用越来越广泛&#xff0c;尤其是在生态环境监测、农业、林业等领域&#xff0c;无人机遥感图像的处理与分析成为了科研和业务化工作中的重要环节。通过无人机获取的高分辨率影像数据&#xff0c;结合图像拼接与处理技术&#xff0c;…

[Linux] 信号(singal)详解(二):信号管理的三张表、如何使用coredump文件、OS的用户态和内核态、如何理解系统调用?

标题&#xff1a;[Linux] 信号管理的三张表、如何使用coredump文件、OS的用户态和内核态、如何理解系统调用&#xff1f; 水墨不写bug &#xff08;图片来源&#xff1a;文心一言&#xff09; 正文开始&#xff1a; 目录 一、信号管理的三张表 &#xff08;1&#xff09;三张表…

Windows中使用Docker安装Anythingllm,基于deepseek构建自己的本地知识库问答大模型,可局域网内多用户访问、离线运行

文章目录 Windows中使用Docker安装Anythingllm&#xff0c;基于deepseek构建自己的知识库问答大模型1. 安装 Docker Desktop2. 使用Docker拉取Anythingllm镜像2. 设置 STORAGE_LOCATION 路径3. 创建存储目录和 .env 文件.env 文件的作用关键配置项 4. 运行 Docker 命令docker r…

w~自动驾驶~合集17

我自己的原文哦~ https://blog.51cto.com/whaosoft/13269720 #FastOcc 推理更快、部署友好Occ算法来啦&#xff01; 在自动驾驶系统当中&#xff0c;感知任务是整个自驾系统中至关重要的组成部分。感知任务的主要目标是使自动驾驶车辆能够理解和感知周围的环境元素&#…

利用邮件合并将Excel的信息转为Word(单个测试用例转Word)

利用邮件合并将Excel的信息转为Word 效果一览效果前效果后 场景及问题解决方案 一、准备工作准备Excel数据源准备Word模板 二、邮件合并操作步骤连接Excel数据源插入合并域预览并生成合并文档 效果一览 效果前 效果后 场景及问题 在执行项目时的验收阶段&#xff0c;对于测试…

2024 CyberHost 语音+图像-视频

项目&#xff1a;CyberHost: Taming Audio-driven Avatar Diffusion Model with Region Codebook Attention 音频驱动的身体动画面临两个主要挑战&#xff1a;&#xff08;1&#xff09;关键人体部位&#xff0c;如面部和手部&#xff0c;在视频帧中所占比例较小&#x…

web前端第三次作业

题目 本期作业 WEB第三次作业 请使用JS实一个网页中登录窗口的显示/隐藏&#xff0c;页面中拖动移动&#xff0c;并且添加了边界判断的网页效 代码图片 效果展示 代码 <!DOCTYPE html> <html lang"zh"> <head> <meta charset"UTF-8&qu…

国产ARM处理器工控机如何助力企业实现自主可控?

选择国产ARM处理器工控机的原因可以从多个角度来考虑&#xff0c;包括技术、经济、安全和政策等方面。以下是一些关键理由&#xff1a; 技术优势 低功耗高效能&#xff1a;ARM架构以其出色的能效比著称&#xff0c;适合需要长时间运行的工业控制应用。适应性强&#xff1a;国…

力扣24题——两两交换链表中节点

#题目 #代码 /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val val; }* ListNode(int val, ListNode next) { this.val val; this.next next; }* }*/ clas…

DedeBIZ系统审计小结

之前简单审计过DedeBIZ系统&#xff0c;网上还没有对这个系统的漏洞有过详尽的分析&#xff0c;于是重新审计并总结文章&#xff0c;记录下自己审计的过程。 https://github.com/DedeBIZ/DedeV6/archive/refs/tags/6.2.10.zip &#x1f4cc;DedeBIZ 系统并非基于 MVC 框架&…

leetocde92:翻转链表II

前文关于反转链表的解析https://blog.csdn.net/weixin_46028606/article/details/145592860?fromshareblogdetail&sharetypeblogdetail&sharerId145592860&sharereferPC&sharesourceweixin_46028606&sharefromfrom_link 翻转链表II 代码一定要结合下面的图…

考研操作系统----操作系统的概念定义功能和目标(仅仅作为王道哔站课程讲义作用)

目录 操作系统的概念定义功能和目标 操作系统的四个特征 操作系统的分类 ​编辑 操作系统的运行机制 系统调用 操作系统体系结构 操作系统引导 虚拟机 操作系统的概念定义功能和目标 什么是操作系统&#xff1a; 操作系统是指控制和管理整个计算机系统的软硬件资源&…

【WB 深度学习实验管理】使用 PyTorch Lightning 实现高效的图像分类实验跟踪

本文使用到的 Jupyter Notebook 可在GitHub仓库002文件夹找到&#xff0c;别忘了给仓库点个小心心~~~ https://github.com/LFF8888/FF-Studio-Resources 在机器学习项目中&#xff0c;实验跟踪和结果可视化是至关重要的环节。无论是调整超参数、优化模型架构&#xff0c;还是监…

异位妊娠唯一相关的是年龄(U型曲线)

异位妊娠唯一相关的是年龄&#xff08;U型曲线&#xff09; 简介 异位妊娠&#xff0c;俗称宫外孕&#xff0c;是指受精卵在子宫体腔以外着床发育的异常妊娠过程 。正常情况下&#xff0c;受精卵会在子宫内着床并发育成胎儿&#xff0c;但在异位妊娠中&#xff0c;受精卵却在…

ESM3(1)-介绍:用语言模型模拟5亿年的进化历程

超过30亿年的进化在天然蛋白质空间中编码形成了一幅生物学图景。在此&#xff0c;作者证明在进化数据上进行大规模训练的语言模型&#xff0c;能够生成与已知蛋白质差异巨大的功能性蛋白质&#xff0c;并推出了ESM3&#xff0c;这是一款前沿的多模态生成式语言模型&#xff0c;…