J.U.C - 深入解析ReentrantLock原理源码

文章目录

  • 概述
  • synchronized的缺陷
    • 1)synchronized不能控制阻塞,不能灵活控制锁的释放。
    • 2)在读多写少的场景中,效率低下。
  • 独占锁ReentrantLock原理
    • ReentrantLock概述
    • AQS同步队列
      • 1. AQS实现原理
      • 2. 线程被唤醒时,AQS队列的变化
    • 锁的获取
      • 1. F. lock
      • 2. AQS. acquire
      • 3. FairSync. tryAcquire
      • 4. AQS. addWaiter(Node. EXCLUSIVE)
      • 5. AQS. acquireQueued(newNode,1)
      • 6. AQS. shouldParkAfterFailedAcquire
      • 7. AQS. parkAndCheckInterrupt:将当前线程阻塞挂起
    • 锁的释放
      • 1. ReentrantLock. unlock
      • 2. AQS. release
      • 3. Sync. tryRelease
      • 4. AQS. signalNext (JDK17)
    • 公平锁和非公平锁实现区别
      • 1. 非公平锁与非公平锁的释放过程没有任何差异
      • 2. 非公平锁与非公平锁获取锁的差异
        • (1)tryAcquire差异 (JDK17)

在这里插入图片描述


概述

Java提供了种类丰富的锁,synchronized为Java的关键字,是Java最早提供的同步机制。当它用来修饰一个方法或一个代码块时,能够保证在同一时刻最多只能有一个线程执行该代码。

当使用synchronized修饰代码时,并不需要显式的执行加锁和解锁过程,所以它也被称之为隐式锁。


除了Java提供的synchronized关键字,从Java 5. 0起,JUC包中引入了显式锁作为synchronized的补充,提供了比synchronized同步锁更广泛的锁定操作,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。

JUC中的锁全部位于Java. util. concurrent. locks下,是并发包中最核心组件之一,是保证线程安全的重要组件。

查看JUC工具类的源码,就会发现JUC中绝大部分组件都用到了它们。

JUC中的锁分为两类:重入锁和读写锁。

重入锁:ReentrantLock类,它是一种可重入的独占锁,具有与使用synchronized相同的一些基本行为和语义,但是它拥有了更广泛的API,它的功能更强大。ReentrantLock相当于synchronized的增强版,具有synchronized很多所没有的功能。

读写锁:读写锁的实现有两个,分别为ReentrantReadWriteLock和StampedLockReentrantReadWriteLock维护了一对关联锁:ReadLock和WriteLock,由词知意,一个读锁一个写锁,合称“读写锁”​。一个是ReadLock(读锁)用于读操作的,一个是WriteLock(写锁)用于写操作。读写锁适合于读多写少的场景,基本原则是读锁可以被多个线程同时持有进行访问,而写锁只能被一个线程持有。

ReentrantReadWriteLock可以使得多个读线程同时持有读锁,而写锁是写线程独占的。读写锁如果使用不当,很容易产生“饥饿”问题:比如在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”​。虽然使用公平策略可以一定程度上缓解这个问题,但是公平策略是以牺牲系统吞吐量为代价的。

StampedLock,是在JDK 1. 8引入的锁类型,是对读写锁ReentrantReadWriteLock的增强版。StampedLock采用一种乐观的读策略,使得读锁完全不会阻塞写线程。

Java锁机制
synchronized
JUC锁
隐式锁
自动加锁/解锁
显式锁
ReentrantLock
ReentrantReadWriteLock
StampedLock
更广泛的锁定操作
可重入的独占锁
功能更强大
一对关联锁: ReadLock, WriteLock
读多写少场景
读锁共享, 写锁独占
可能产生饥饿问题
JDK 1.8引入
乐观读策略
读锁不阻塞写线程

synchronized的缺陷

Java已经提供了synchronized同步锁可以对临界资源同步互斥访问,为什么还要使用JUC锁呢?

主要是因为synchronized同步锁存在以下两种问题:

1)synchronized不能控制阻塞,不能灵活控制锁的释放。

synchronized同步锁提供了一种排他式的同步机制,当多个线程竞争锁资源时,同时只能有一个线程持有锁,当一个线程获取了锁,其他线程就会被阻塞,只有等占有锁的线程释放锁后,才能重新进行竞争锁。

当使用synchronized同步锁,线程会在三种情况下释放锁:

  • ① 线程执行完同步代码块/方法,释放锁;
  • ② 线程执行时发生异常,此时JVM会让线程自动释放锁;
  • ③ 在同步代码块/方法中,锁对象执行了wait方法,线程释放锁。

使用synchronized同步锁,假如占有锁的线程被长时间阻塞(IO阻塞、sleep方法、join方法等)​,由于线程在阻塞时不会释放锁,一旦其他线程此时尝试获取锁,就会被阻塞而一直等待下去,甚至可能会发生死锁,这样就会造成大量线程的堆积,严重降低服务器的性能。

所以synchronized同步锁的线程阻塞,存在两个致命的缺陷:无法控制阻塞时长,阻塞不可中断

JUC锁可以解决这种情况。

  • 使用ReentrantLock重入锁时,线程可以使用tryLock(long time、TimeUnit unit)方法获取锁,限定一个获取锁的超时时间,如果在规定的时间内获取锁失败,线程就会放弃锁的获取。
  • 也可以使用lockInterruptibly()方法获取锁,如果线程长时间没有获取锁,在等待时如果其他线程调用该线程的interrupt方法中断线程,该线程就可以通过响应中断放弃获取锁。

2)在读多写少的场景中,效率低下。

在读多写少的场景中,当多个读线程同时操作共享资源时,读操作不会对共享资源进行修改,所以读线程和读线程是不需要同步的。

如果这时采用synchronized关键字,就会导致一个问题,当多个线程都只进行读操作时,所有线程都只能同步进行,只能有一个读线程可以进行操作,其他读线程只能被阻塞而无法进行读操作。

因此,在读多写少的场景中,我们需要实现一种机制,当多个线程都只是进行读操作时,使得线程可以同时进行读操作。ReentrantReadWriteLock锁可以解决这种情况。所以通过JUC锁可以很容易解决以上问题,但synchronized同步锁却对此无能为力。

在这里插入图片描述


独占锁ReentrantLock原理

ReentrantLock概述

ReentrantLock 是一个显式的互斥锁,提供比 synchronized 关键字更强大的功能,例如可中断、超时和公平性选择。其底层基于 AbstractQueuedSynchronizer (AQS),其主要作用是控制线程的同步。

在这里插入图片描述

从图可以看到ReentrantLock实现了Lock接口,在ReentrantLock中有非公平锁NonfairSync和公平锁FairSync的实现,NonfairSync和FairSync都继承自抽象类Sync。

Sync类是ReentrantLock的内部抽象类,继承自抽象类AbstractQueuedSynchronizer(简称AQS)​。

如果大家看过JUC的源代码,会发现不仅重入锁用到了AQS,JUC中绝大部分的同步工具也都是基于AQS构建的。

Lock接口定义了一套锁实现的标准规范,定义了获得锁和释放锁一系列方法,所以Lock锁可以提供比synchronized更广泛的锁操作,可以更灵活地控制锁的粒度.

内部抽象类
Lock
+lock()
+unlock()
+tryLock()
+tryLock(long time, TimeUnit unit)
+newCondition()
ReentrantLock
-sync: Sync
+lock()
+unlock()
+tryLock()
+tryLock(long time, TimeUnit unit)
+newCondition()
Sync
<#AbstractQueuedSynchronizer>
NonfairSync
-sync: Sync
+tryAcquire(int acquires)
FairSync
-sync: Sync
+tryAcquire(int acquires)
AbstractQueuedSynchronizer
+acquire(int arg)
+release(int arg)
+tryAcquire(int arg)
+tryRelease(int arg)

ReentrantLock中提供了两个构造器:其中一个构造器可以指定锁的类型公平锁/非公平锁,默认构造器默认使用非公平锁

  • 公平锁:多个线程按照申请锁的顺序,按照先来后到的原则获得锁。
  • 非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,允许“插队”​,有可能出现后申请的线程比先申请的线程优先获取锁的情况。

一般情况下使用公平锁的程序在多线程访问时,在线程调度上面的开销比较大,所以总体吞吐量会比较低。

ReentrantLock和synchronized一样都是独占排他锁,所有的线程都是同步互斥的访问同步块代码。同时只能有一个线程获得锁,其他线程只能等待锁被释放后才能有机会获得锁的使用权。

ReentrantLock锁的使用比较简单

import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {private final ReentrantLock lock = new ReentrantLock();public void doSomething() {lock.lock(); // 获取锁try {// 执行临界区代码System.out.println("执行临界区代码");} finally {lock.unlock(); // 释放锁}}public static void main(String[] args) {ReentrantLockExample example = new ReentrantLockExample();example.doSomething();}
}

使用一个示例来感受下ReentrantLock的同步效果,代码如下所示

import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {private final ReentrantLock lock = new ReentrantLock();public void doSomething() {lock.lock(); // 获取锁try {// 执行临界区代码System.out.println(Thread.currentThread().getName() + " 进入临界区");Thread.sleep(2000); // 模拟耗时操作System.out.println(Thread.currentThread().getName() + " 离开临界区");} catch (InterruptedException e) {Thread.currentThread().interrupt();System.out.println(Thread.currentThread().getName() + " 被中断");} finally {lock.unlock(); // 释放锁}}public static void main(String[] args) {ReentrantLockExample example = new ReentrantLockExample();Thread thread1 = new Thread(() -> example.doSomething(), "Thread1");Thread thread2 = new Thread(() -> example.doSomething(), "Thread2");thread1.start();thread2.start();}
}

输出

Thread1 进入临界区
Thread1 离开临界区
Thread2 进入临界区
Thread2 离开临界区

Thread1先于Thread2执行lock. lock(),会先获得锁,随后Thread2尝试获得锁会被阻塞,Thread1运行完毕需要执行lock. unlock()释放锁,这时Thread2才能通过lock. lock()获得锁运行同步代码块

Thread1 Thread2 Lock lock.lock() 获得锁 执行临界区代码 lock.unlock() 释放锁 lock.lock() 被阻塞 完成临界区代码 lock.unlock() 获得锁 执行临界区代码 lock.unlock() 释放锁 Thread1 Thread2 Lock

AQS同步队列

重入锁ReentrantLock的实现底层是使用AbstractQueuedSynchronizer实现的。Abstract-QueuedSynchronizer是一个抽象同步类,简称AQS。它是实现JUC中绝大部分同步工具的核心组件,都是基于AQS构建的.

AQS提供了一套通用的机制来管理同步状态、阻塞/唤醒线程、管理等待队列等。除了JUC中的锁,JUC中的其他同步工具如CountDownLatch、CyclicBarrier等,也都是通过内部类实现了AQS的API,来实现各自的同步器功能。如果掌握了AQS,那JUC中绝大多数的工具类也就可以轻松掌握.

内部抽象类
AbstractQueuedSynchronizer
+int getState()
+void setState(int newState)
+boolean compareAndSetState(int expect, int update)
+void acquire(int arg)
+void acquireInterruptibly(int arg)
+boolean tryAcquire(int arg)
+boolean release(int arg)
+boolean tryRelease(int arg)
+Condition newCondition()
ReentrantLock
-sync: Sync
+lock()
+unlock()
+tryLock()
+tryLock(long time, TimeUnit unit)
+newCondition()
Sync
<#AbstractQueuedSynchronizer>
NonfairSync
-sync: Sync
+tryAcquire(int acquires)
FairSync
-sync: Sync
+tryAcquire(int acquires)
Semaphore
-sync: Sync
+acquire()
+release()
CountDownLatch
-sync: Sync
+await()
+countDown()
CyclicBarrier
-sync: Sync
+await()

AQS 是 ReentrantLock 等同步器的基础。它通过一个同步队列来管理线程的等待和唤醒。AQS 通过维护一个双向链表来表示等待线程的顺序.

内部类
内部抽象类
AbstractQueuedSynchronizer
-state: int
-head: Node
-tail: Node
+int getState()
+void setState(int newState)
+boolean compareAndSetState(int expect, int update)
+void acquire(int arg)
+void acquireInterruptibly(int arg)
+boolean tryAcquire(int arg)
+boolean release(int arg)
+boolean tryRelease(int arg)
+Condition newCondition()
+addWaiter(Node mode)
+enqueue(Node node)
+acquireQueued(Node node, int arg)
+shouldParkAfterFailedAcquire(Node pred, Node node)
+parkAndCheckInterrupt()
+selfInterrupt()
+setHead(Node node)
+setHeadAndTail(Node node, Node tail)
+doAcquireShared(int arg)
+doAcquireSharedInterruptibly(int arg)
+doReleaseShared()
Node
-thread: Thread
-prev: Node
-next: Node
-waitStatus: int
+static final int SHARED = 1
+static final int EXCLUSIVE = 0
+static final int CANCELLED = 1
+static final int SIGNAL = -1
+static final int CONDITION = -2
+static final int PROPAGATE = -3
AbstractOwnableSynchronizer
-exclusiveOwnerThread: Thread
+void setExclusiveOwnerThread(Thread current)
+Thread getExclusiveOwnerThread()
ReentrantLock
-sync: Sync
+lock()
+unlock()
+tryLock()
+tryLock(long time, TimeUnit unit)
+newCondition()
Sync
<#AbstractQueuedSynchronizer>
NonfairSync
-sync: Sync
+tryAcquire(int acquires)
FairSync
-sync: Sync
+tryAcquire(int acquires)

AQS是一个FIFO的双向队列,其内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node。

内部类
AbstractQueuedSynchronizer
-state: int
-head: Node
-tail: Node
+int getState()
+void setState(int newState)
+boolean compareAndSetState(int expect, int update)
+void acquire(int arg)
+void acquireInterruptibly(int arg)
+boolean tryAcquire(int arg)
+boolean release(int arg)
+boolean tryRelease(int arg)
+Condition newCondition()
+addWaiter(Node mode)
+enqueue(Node node)
+acquireQueued(Node node, int arg)
+shouldParkAfterFailedAcquire(Node pred, Node node)
+parkAndCheckInterrupt()
+selfInterrupt()
+setHead(Node node)
+setHeadAndTail(Node node, Node tail)
+doAcquireShared(int arg)
+doAcquireSharedInterruptibly(int arg)
+doReleaseShared()
Node
-thread: Thread
-prev: Node
-next: Node
-waitStatus: int
+static final int SHARED = 1
+static final int EXCLUSIVE = 0
+static final int CANCELLED = 1
+static final int SIGNAL = -1
+static final int CONDITION = -2
+static final int PROPAGATE = -3

其中

  • Node中的thread变量用来存放进入AQS队列里面的线程;

  • Node节点内部的prev记录当前节点的前驱节点,next记录当前节点的后继节点;

  • SHARED用来标记该线程是获取共享资源时被阻塞挂起后放入AQS队列的,

  • EXCLUSIVE用来标记线程是获取独占资源时被挂起后放入AQS队列的;

  • waitStatus记录当前线程等待状态,可以为CANCELLED(取消线程)​、SIGNAL(线程需要被唤醒)​、CONDITION(线程在条件队列里面等待)​、PROPAGATE(释放共享资源时需要通知其他节点

1. AQS实现原理

AQS维护同步队列,内部使用一个FIFO的双向链表,管理线程同步时的所有被阻塞线程。双向链表这种数据结构,它的每个数据节点中都有两个指针,分别指向直接后继节点和直接前驱节点。所以,从双向链表中的任意一个节点开始,都可以很方便地访问它的前驱节点和后继节点。

AQS的同步队列内部结构如下图所示,AQS中的内部静态类Node为链表节点,AQS会在线程获取锁失败后,线程会被阻塞并被封装成Node加入到AQS队列中;当获取锁的线程释放锁后,会从AQS队列中唤醒一个线程(节点)​。

AQS同步队列结构
AQS同步队列结构

线程抢夺锁失败时加入AQS,AQS队列的变化如下

1)AQS的head、tail分别代表同步队列头节点和尾节点指针,默认为null,如下图所示【AQS初始状态】。

在这里插入图片描述

AQS head = null tail = null AQS

2)当第一个线程抢夺锁失败,同步队列会先初始化,随后线程会被封装成Node节点追加到AQS队列中 【AQS初始化】

在这里插入图片描述

AQS ThreadA NodeA 抢夺锁失败 初始化同步队列 创建NodeA head = NodeA tail = NodeA AQS ThreadA NodeA

3)接下来将ThreadB封装成Node节点,追加到AQS队列。

设置新节点的prev指向AQS队尾节点;将队尾节点的next指向新节点;最后将AQS尾节点指针指向新节点。此时AQS变化 【ThreadB阻塞加入AQS】

在这里插入图片描述

AQS ThreadB NodeB tail 抢夺锁失败 创建NodeB NodeB.prev = tail tail.next = NodeB tail = NodeB AQS ThreadB NodeB tail

4)当下一个线程抢夺锁失败时,重复上面步骤即可。将线程封装成Node,追加到AQS队列

假设此次抢占锁失败的线程为ThreadC,此时AQS变为如下 【ThreadC阻塞加入AQS】

在这里插入图片描述

AQS ThreadC NodeC tail 抢夺锁失败 创建NodeC NodeC.prev = tail tail.next = NodeC tail = NodeC AQS ThreadC NodeC tail

2. 线程被唤醒时,AQS队列的变化

ReentrantLock唤醒阻塞线程时,会按照FIFO的原则从AQS中head头部开始唤醒首个节点中线程。head节点表示当前获取锁成功的线程ThreadA节点。当ThreadA释放锁时,它会唤醒后继节点线程ThreadB,ThreadB开始尝试获得锁,如果ThreadB获得锁成功,会将自己设置为AQS的头节点。ThreadB获取锁成功后,此时AQS变化,如图

在这里插入图片描述

  • 1)head指针指向ThreadB节点。
  • 2)将原来头节点的next指向Null,从AQS中删除。
  • 3)将ThreadB节点的prev指向Null,设置节点的thread=null。
AQS ThreadA ThreadB NodeA NodeB 释放锁 唤醒后继节点ThreadB 尝试获取锁 获取锁成功 head = NodeB NodeA.next = null NodeB.prev = null NodeA.thread = null AQS ThreadA ThreadB NodeA NodeB

线程被唤醒时AQS队列的变化:

  • ThreadA 释放锁。
  • AQS 唤醒 ThreadA 的后继节点 ThreadB。
  • ThreadB 尝试获取锁。
  • ThreadB 获取锁成功。
  • AQS 更新 head 指针,使其指向 NodeB。
  • 将原头节点 NodeA 的 next 指向 null,从AQS中删除。
  • 将 NodeB 的 prev 指向 null。
  • 将 NodeA 的 thread 设置为 null

以上就是线程在竞争锁时,线程被阻塞和被唤醒时AQS同步队列的基本实现过程。


锁的获取

AQS使用了一种典型的设计模式:模板方法。AQS中大多数方法都是final或private的,也就是说AQS并不希望用户覆盖或直接使用这些方法,只能重写AQS规定的部分方法。

使用模板方法时,父类(AQS框架)定义好一个操作算法的骨架,而一些步骤延迟到子类去实现。这样子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。通俗地讲,就是将子类相同的方法,都放到其抽象父类中。


下面,以锁中相对简单的公平锁实现为例,以获取锁的lock方法为入口,分析多线程是如何同步获取ReentrantLock锁的

1. F. lock

/*** 获取锁。** <p>如果锁没有被其他线程持有,则立即获取锁并返回,将锁的持有计数设置为1。** <p>如果当前线程已经持有锁,则将锁的持有计数增加1,并立即返回。** <p>如果锁被其他线程持有,则当前线程将被禁用以进行线程调度,* 并处于休眠状态,直到获取到锁为止,此时锁的持有计数将被设置为1。*/
public void lock() {sync.lock();
}

可以看到ReentrantLock获取锁调用了lock方法,该方法的内部调用了sync. lock()。

sync是Sync类的一个实例,Sync类是ReentrantLock的抽象静态内部类,它集成了AQS来实现重入锁的具体业务逻辑。AQS是一个同步队列,实现了线程的阻塞和唤醒,没有实现具体的业务功能。在不同的同步场景中,需要用户继承AQS来实现对应的功能

在这里插入图片描述

Sync有两个实现类公平锁FairSync和非公平锁NoFairSync.

    /*** Creates an instance of {@code ReentrantLock} with the* given fairness policy.** @param fair {@code true} if this lock should use a fair ordering policy*/public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}

重入锁实例化时,可以根据参数fair为属性sync创建对应锁的实例。以公平锁为例,调用sync. lock事实上调用的是FairSync的lock方法

在这里插入图片描述
该方法的内部执行了方法

在这里插入图片描述

acquire为AQS中的final方法,用于竞争锁


2. AQS. acquire

线程进入AQS中的acquire方法,arg=1。arg参数作用:在重入锁中,计算重入次数时使用。获取锁成功后,锁状态标识state=state+arg

/*** 以独占模式获取锁,忽略中断。通过至少调用一次 {@link #tryAcquire} 实现,* 如果获取成功则返回。否则,线程将被排队,可能多次阻塞和解除阻塞,直到调用 {@link #tryAcquire} 成功。* 此方法可以用于实现 {@link Lock#lock} 方法。** @param arg 获取参数。此值传递给 {@link #tryAcquire},但除此之外没有特定的解释,可以表示任何你想要的内容。*/
public final void acquire(int arg) {// 尝试获取锁if (!tryAcquire(arg) &&// 如果获取失败,将当前线程加入等待队列,并尝试获取锁直到成功acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 如果在等待过程中被中断,则恢复中断状态selfInterrupt();
}

这个方法的逻辑为:先尝试抢占锁,抢占成功,直接返回。抢占失败,将线程封装成Node节点,追加到AQS队列中并使线程阻塞等待。

  • 1)首先会执行tryAcquire(1)尝试抢占锁,成功返回true,失败返回false。
  • 2)抢占锁失败后,执行addWaiter(Node.EXCLUSIVE)将线程封装成Node节点追加到AQS队列。
  • 3)然后调用acquireQueued将线程阻塞。
  • 4)线程阻塞后,接下来就只需等待其他线程(其他线程释放锁时)唤醒它,线程被唤醒后会重新竞争锁的使用。
CurrentThread Sync AQS acquire(int arg) tryAcquire(arg) 返回 addWaiter(Node.EXCLUSIVE) 将线程封装成Node节点追加到AQS队列 acquireQueued(newNode, arg) shouldParkAfterFailedAcquire(pred, newNode) parkAndCheckInterrupt() 继续检查 alt [需要阻塞] [不需要阻塞] loop [等待被唤醒] 返回 继续等待 alt [被唤醒后获取锁成功] [被唤醒后获取锁失败] alt [tryAcquire 成功] [tryAcquire 失败] CurrentThread Sync AQS

3. FairSync. tryAcquire

尝试获取锁。若获取锁成功,返回true;获取锁失败,返回false

/*** 公平版本的 tryAcquire。除非是递归调用、没有等待者或当前线程是第一个等待者,否则不授予访问权限。*/
protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread(); // 获取当前线程int c = getState(); // 获取当前同步状态(即锁的状态)// 如果当前状态为0,表示锁未被任何线程持有if (c == 0) {// 检查是否有前驱节点(即是否有其他线程在等待)if (!hasQueuedPredecessors() &&// 使用 CAS 操作将状态从0设置为acquirescompareAndSetState(0, acquires)) {setExclusiveOwnerThread(current); // 设置当前线程为锁的持有者return true; // 获取锁成功}}// 如果当前线程已经是锁的持有者(递归调用)else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires; // 计算新的锁状态if (nextc < 0)throw new Error("Maximum lock count exceeded"); // 锁的持有计数超过最大值setState(nextc); // 更新锁状态return true; // 获取锁成功}return false; // 获取锁失败
}

这个方法逻辑:获取当前的锁状态,如果为无锁状态,当前线程会执行CAS操作尝试获取锁;若当前线程是重入获取锁,只需增加锁的重入次数即可

  • 1)首先通过getState()会获取锁的状态state的值,如果state=0表示当前锁为无锁状态。通过CAS更新state的值为1,若CAS成功说明当前线程获取锁成功,直接返回true。若CAS失败,说明锁已经被其他线程占用,当前线程获取锁失败,返回false。
  • 2)如果state>0,表示当前锁已被占用并且占用锁线程是否为当前线程。增加锁重入次数,获取锁成功,直接返回true。
  • 3)否则说明锁被其他线程占有,抢占锁失败,线程需要等待锁被释放。

CAS更新state的值为1时,使用了AQS的compareAndSetState方法,这个方法比较并替换state的值,如果当前对象state的值和预期值0相等,则将state的值替换为1,否则不替换。替换成功返回true,替换失败返回false。这个方法是原子操作,其实底层使用了Unsafe这个类的compareAndSwapInt方法,所以不存在线程安全问题.

state是AQS同步队列中的一个属性,表示一个重入锁的锁状态。① state=0:表示无锁状态。② state>0:表示锁已经被线程占有,同时state的值还代表重入次数。

ReentrantLock是重入锁,允许同一个线程多次获得同一个锁。线程初次获得锁时state=1,再次获得锁时state会递增加1,state=state+1。比如重入3次,那么state=3。而在释放锁的时候,必须释放相同的次数,state=0时其他线程才能获得锁

CurrentThread Sync tryAcquire(int acquires) 获取当前线程 (Thread.currentThread()) 获取当前同步状态 (getState()) 判断当前状态是否为0 (c == 0) 检查是否有前驱节点 (hasQueuedPredecessors()) 使用CAS操作设置状态 (compareAndSetState(0, acquires)) 设置当前线程为锁的持有者 (setExclusiveOwnerThread(current)) 返回true 返回false alt [CAS操作成功] [CAS操作失败] 返回false alt [没有前驱节点] [有前驱节点] 判断当前线程是否为锁的持有者 (current == getExclusiveOwnerThread()) 计算新的锁状态 (nextc = c + acquires) 检查新的锁状态是否小于0 (nextc < 0) 抛出错误 (Maximum lock count exceeded) 更新锁状态 (setState(nextc)) 返回true alt [新的锁状态小于0] [新的锁状态不小于0] 返回false alt [当前线程是锁的持有者] [当前线程不是锁的持有者] alt [当前状态为0] [当前状态不为0] CurrentThread Sync

4. AQS. addWaiter(Node. EXCLUSIVE)

线程抢占锁失败后,执行addWaiter(Node.EXCLUSIVE)将线程封装成Node节点追加到AQS队列中。

/*** 为当前线程和给定模式创建并入队一个节点。** @param mode Node.EXCLUSIVE 表示独占模式,Node.SHARED 表示共享模式* @return 新创建的节点*/
private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode); // 创建新节点// 尝试快速路径入队;如果失败则使用完整入队方法Node pred = tail; // 获取尾节点if (pred != null) { // 如果尾节点不为空node.prev = pred; // 设置新节点的前驱节点为当前尾节点if (compareAndSetTail(pred, node)) { // 使用 CAS 操作将尾节点设置为新节点pred.next = node; // 设置当前尾节点的后继节点为新节点return node; // 返回新节点}}enq(node); // 如果 CAS 操作失败,使用完整入队方法return node; // 返回新节点
}
  • 1)将当前线程封装成Node节点。
  • 2)若AQS同步队列的tail节点不为null,将当前线程节点追加到链表中。
  • 3)若同步队列为空,初始化链表并将当前线程追加到链表中。
CurrentThread Sync Node addWaiter(Node mode) 创建新节点 (new Node(Thread.currentThread(), mode)) 获取尾节点 (tail) 设置新节点的前驱节点 (node.prev = pred) 使用CAS操作设置尾节点 (compareAndSetTail(pred, node)) 设置当前尾节点的后继节点 (pred.next = node) 返回新节点 (node) 使用完整入队方法 (enq(node)) 返回新节点 (node) alt [CAS操作成功] [CAS操作失败] 使用完整入队方法 (enq(node)) 返回新节点 (node) alt [尾节点不为空 (pred != null)] [尾节点为空] CurrentThread Sync Node

addWaiter(Node mode)中的mode参数表示节点的类型,Node.EXCLUSIVE表示是独占排他锁,也就是说重入锁是独占锁,用到了AQS的独占模式

Node定义了两种节点类型:

  • 1)共享模式:Node. SHARED。共享模式,可以被多个线程同时持有,如读写锁的读锁
  • 2)独占模式: Node. EXCLUSIVE。独占很好理解,即自己独占资源,如独占排他锁同时只能由一个线程持有。

所以,相应的AQS支持两种模式:独占模式和共享模式


5. AQS. acquireQueued(newNode,1)

这个方法的主要作用就是将线程阻塞。参数node为新加入等待队列线程节点。

/*** 以独占且不可中断模式获取锁,适用于已经在队列中的线程。条件等待方法和获取锁方法都会使用此方法。** @param node 节点* @param arg 获取锁的参数* @return 如果在等待期间被中断,则返回 {@code true}*/
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; // 帮助垃圾回收failed = false; // 标记获取锁成功return interrupted; // 返回是否在等待期间被中断}if (shouldParkAfterFailedAcquire(p, node) && // 检查是否需要阻塞当前线程parkAndCheckInterrupt()) // 阻塞当前线程并检查是否被中断interrupted = true; // 标记在等待期间被中断}} finally {if (failed) // 如果获取锁失败cancelAcquire(node); // 取消获取锁}
}

主要逻辑如下

  • 1)若同步队列中,若当前节点为队列第一个线程,则有资格竞争锁,再次尝试获得锁。
  • 2)尝试获得锁成功,移除链表head节点,并将当前线程节点设置为head节点。
  • 3)尝试获得锁失败,判断是否需要阻塞当前线程。
  • 4)若发生异常,取消当前线程获得锁的资格
CurrentThread Sync Node acquireQueued(final Node node, int arg) 初始化失败标记 (boolean failed = true) 初始化中断标记 (boolean interrupted = false) 获取当前节点的前驱节点 (final Node p = node.predecessor()) 将当前节点设置为头节点 (setHead(node)) 清除前驱节点的后继指针 (p.next = null) 标记获取锁成功 (failed = false) 返回是否在等待期间被中断 (interrupted) 检查是否需要阻塞当前线程 (shouldParkAfterFailedAcquire(p, node)) 阻塞当前线程并检查是否被中断 (parkAndCheckInterrupt()) 标记在等待期间被中断 (interrupted = true) alt [需要阻塞] alt [前驱节点是头节点且成功获取锁 (p == head && tryAcquire(arg))] loop [无限循环] 检查是否获取锁失败 (if (failed)) 取消获取锁 (cancelAcquire(node)) alt [获取锁失败] CurrentThread Sync Node

6. AQS. shouldParkAfterFailedAcquire

这个方法的主要作用是:线程竞争锁失败以后,通过Node的前驱节点的waitStatus状态来判断,线程是否需要被阻塞

/*** 检查并更新未能获取锁的节点的状态。如果线程应该阻塞,则返回 true。* 这是在所有获取循环中的主要信号控制。要求 pred == node.prev。** @param pred 节点的前驱节点,持有状态* @param node 节点* @return 如果线程应该阻塞,则返回 {@code true}*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus; // 获取前驱节点的状态if (ws == Node.SIGNAL) {/** 该节点已经设置了状态,请求释放时通知它,因此它可以安全地阻塞。*/return true;}if (ws > 0) {/** 前驱节点已被取消。跳过前驱节点并指示重试。*/do {node.prev = pred = pred.prev; // 更新当前节点的前驱节点} while (pred.waitStatus > 0); // 继续跳过已取消的前驱节点pred.next = node; // 更新前驱节点的后继节点} else {/** waitStatus 必须是 0 或 PROPAGATE。指示我们需要一个信号,但还不阻塞。* 调用者需要重试以确保在阻塞之前无法获取锁。*/compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 使用 CAS 操作设置前驱节点的状态为 SIGNAL}return false; // 返回 false,表示不应该阻塞
}
  • 1)如果前驱节点状态为SIGNAL,当前线程可以被放心的阻塞,返回true。
  • 2)若前驱节点状态为CANCELLED,向前扫描链表把CANCELLED状态的节点从同步队列中移除,返回false。最终结果,当前节点的前驱节点为非取消状态。之后,当前线程执行轨迹如下:① 再次返回方法acquireQueued,再次循环,尝试获取锁。② 再次执行shouldParkAfterFailedAcquire判断是否需要阻塞。
  • 3)若前驱节点状态为默认状态或PROPAGATE,修改前驱节点的状态为SIGNAL,返回false。之后,当前线程执行轨迹如下:① 再次返回方法acquireQueued,再次循环,尝试获取锁。② 再次执行shouldParkAfterFailedAcquire判断是否需要阻塞。③ 当前节点前驱节点为SIGNAL状态,可以放心被阻塞。

根据shouldParkAfterFailedAcquire返回值的不同,线程会继续执行不同的操作。

  • 1)若返回false,会退回到acquireQueued方法,重新执行自旋操作。自旋会重复执行acquireQueued和shouldParkAfterFailedAcquire,有两个结果:① 线程尝试获得锁成功或者线程异常,退出acquireQueued,直接返回。② 若执行shouldParkAfterFailedAcquire成功,当前线程可以被阻塞。
  • 2)若返回true,调用parkAndCheckInterrupt阻塞当前线程。
CurrentThread Sync Node shouldParkAfterFailedAcquire(Node pred, Node node) 获取前驱节点的状态 (int ws = pred.waitStatus) 返回 true (可以安全地阻塞) 更新当前节点的前驱节点 (node.prev = pred = pred.prev) 检查前驱节点的状态 (while (pred.waitStatus > 0)) loop [跳过已取消的前驱节点] 更新前驱节点的后继节点 (pred.next = node) 返回 false (不应该阻塞) 使用 CAS 操作设置前驱节点的状态为 SIGNAL (compareAndSetWaitStatus(pred, ws, Node.SIGNAL)) 返回 false (不应该阻塞) alt [状态为 SIGNAL (ws == Node.SIGNAL)] [状态大于 0 (ws > 0)] [状态为 0 或 PROPAGATE] CurrentThread Sync Node

在AQS定义了Node的5种状态,分别是:

  • ①0:默认状态。
  • ②1:CANCELLED,取消/结束状态。表明线程已取消争抢锁。线程等待超时或者被中断,节点的waitStatus为CANCELLED,线程取消获取锁请求。需要从同步队列中删除该节点。
  • ③ -1:SIGNAL,通知。状态为SIGNAL节点中的线程释放锁时,就会通知后续节点的线程。
  • ④ -2:CONDITION,条件等待。表明节点当前线程在condition队列中。
  • ⑤ -3:PROPAGATE,传播。

在一个节点成为头节点之前,是不会跃迁为P ROPAGATE状态的。用于将唤醒后继线程传递下去,这个状态的引入是为了完善和增强共享锁的唤醒机制.


7. AQS. parkAndCheckInterrupt:将当前线程阻塞挂起

LockSupport. park(this)会阻塞当前线程,会使当前线程(如ThreadB)处于等待状态,不再往下执行。

/*** 方便方法,用于阻塞当前线程并检查是否被中断。** @return 如果线程被中断,则返回 {@code true}*/
private final boolean parkAndCheckInterrupt() {LockSupport.park(this); // 阻塞当前线程return Thread.interrupted(); // 检查并清除线程的中断状态
}
  • 1)当其他线程调用了LockSupport. unpark(ThreadB)​,当前线程才能接着往下执行。
  • 2)若在等待过程中,若其他线程调用了ThreadB.interrupt(),则此时ThreadB的中断标识为true。当前线程需要响应中断请求,将中断标识复位为false(初始状态)​,并且将中断标识返回。
  • 3)acquireQueued线程获取锁成功后,会同时将中断标识返回。
  • 4)若中断标识为false,回到acquire方法,会直接返回。
  • 5)若中断标识为true,回到acquire方法,会执行AQS.selfInterrupt(),将线程中断状态复位。后面当前线程获得锁成功,在处理业务代码时可以检查中断标志的状态来判断是否需要终止当前线程
CurrentThread Sync LockSupport Thread parkAndCheckInterrupt() park(this) // 阻塞当前线程 线程被阻塞 interrupted() // 检查并清除线程的中断状态 返回中断状态 (boolean) 返回中断状态 (boolean) CurrentThread Sync LockSupport Thread

锁的释放

AQS公平锁的释放时序如下

在这里插入图片描述

1. ReentrantLock. unlock

    /*** 尝试释放此锁。** <p>如果当前线程是此锁的持有者,则持有计数会递减。如果持有计数现在为零,则锁被释放。如果当前线程不是此锁的持有者,则抛出 {@link IllegalMonitorStateException}。** @throws IllegalMonitorStateException 如果当前线程未持有此锁*/public void unlock() {sync.release(1);}

释放锁时,需调用ReentrantLock的unlock方法。这个方法内部,会调用 sync.release(1),release方法为AQS类的final方法


2. AQS. release

    /*** 以独占模式释放锁。如果 {@link #tryRelease} 返回 true,则通过解除一个或多个线程的阻塞来实现。* 此方法可用于实现 {@link Lock#unlock} 方法。** @param arg 释放参数。此值传递给 {@link #tryRelease},但除此之外没有特定含义,可以表示任何内容。* @return 从 {@link #tryRelease} 返回的值。*/public final boolean release(int arg) {if (tryRelease(arg)) {  //释放锁 若释放后锁状态为无锁状态,需唤醒后继线程signalNext(head); //同步队列头节点return true;}return false;}

先执行方法tryRelease(1)​。tryRelease方法为ReentrantLock中Sync类的final方法,用于释放锁。释放锁成功后, signalNext(head)会从同队队列中唤醒一个线程重新尝试获取锁.


3. Sync. tryRelease

在这里插入图片描述

   @ReservedStackAccessprotected final boolean tryRelease(int releases) {// 计算新的状态值,即当前状态减去释放次数int c = getState() - releases;// 检查当前线程是否是锁的持有者if (getExclusiveOwnerThread() != Thread.currentThread())throw new IllegalMonitorStateException();// 判断新的状态值是否为零boolean free = (c == 0);// 如果新的状态值为零,表示锁已完全释放if (free)setExclusiveOwnerThread(null); // 清除锁的持有者线程// 更新锁的状态setState(c);// 返回是否锁已完全释放return free;}

尝试释放锁,释放成功返回true,失败返回false。

  • 1)判断当前线程是否为锁持有者,若不是持有者,不能释放锁,直接抛出异常。
  • 2)若当前线程是锁的持有者,将重入次数减1,并判断当前线程是否完全释放了锁。
  • 3)若重入次数为0,则当前新线程完全释放了锁,将锁拥有线程设置为null,并将锁状态置为无锁状态(state=0)​,返回true。
  • 4)若重入次数>0,则当前新线程仍然持有锁,设置重入次数=重入次数-1,返回false. 返回true说明,当前锁被完全释放,这时需要唤醒同步队列中的一个线程,执行unparkSuccessor唤醒同步队列中的节点线程。

4. AQS. signalNext (JDK17)

    /*** 唤醒给定节点的后继节点(如果存在),并取消其 WAITING 状态以避免 park 竞争条件。* 当一个或多个线程被取消时,这可能会失败,但 cancelAcquire 确保活跃性。*/private static void signalNext(Node h) {Node s;// 检查给定节点不为空,且其后继节点不为空,且后继节点的状态不为0if (h != null && (s = h.next) != null && s.status != 0) {// 取消后继节点的 WAITING 状态s.getAndUnsetStatus(WAITING);// 唤醒后继节点的等待线程LockSupport.unpark(s.waiter);}}

唤醒后继线程


公平锁和非公平锁实现区别

公平锁的获取锁和释放锁的流程我们已经梳理完了,公平锁和非公平锁在获取锁和释放锁时有什么区别?

1. 非公平锁与非公平锁的释放过程没有任何差异

释放锁时调用的方法都是AQS的方法,所以非公平锁与非公平锁的释放过程没有任何差异


2. 非公平锁与非公平锁获取锁的差异

公平锁实现中,线程获得锁的顺序是按照请求锁的顺序,按照“先来后到”的规则获取锁。如果线程竞争公平锁失败后,则都会到AQS同步队列队尾排队,将自己阻塞等待锁的使用资格,锁被释放后,会从队首开始查找可以获得锁的线程并唤醒。

而在非公平锁中,允许新线程请求锁时“插队”​,不管AQS同步队列是否有线程在等待,新线程都会先尝试获取锁,如果获取锁失败后,才会进入AQS同步队列队尾排队。

主要差异有以下两处

(1)tryAcquire差异 (JDK17)

ReentrantLock.lock

 @ReservedStackAccessfinal void lock() {if (!initialTryLock())acquire(1);}

AQS.acquire

 public final void acquire(int arg) {if (!tryAcquire(arg))acquire(null, arg, false, false, false, 0L);}

在这里插入图片描述


公平锁

        /*** Acquires only if thread is first waiter or empty*/protected final boolean tryAcquire(int acquires) {if (getState() == 0 && !hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(Thread.currentThread());return true;}return false;}}

公平锁获取锁,若锁为无锁状态时,本着公平原则,新线程在尝试获得锁前,需先判断AQS同步队列中是否有线程在等待(hasQueuedPredecessors),若有线程在等待,当前线程只能进入同步队列等待。若AQS同步无线程等待,则通过CAS抢占锁compareAndSetState

非公平锁

/*** Acquire for non-reentrant cases after initialTryLock prescreen*/protected final boolean tryAcquire(int acquires) {if (getState() == 0 && compareAndSetState(0, acquires)) {setExclusiveOwnerThread(Thread.currentThread());return true;}return false;}

非公平锁,不管AQS是否有线程在等待,若当前锁状态为无锁状态,则会先通过CAS抢占锁(compareAndSetState)。

公平锁和非公平锁获取锁时,其他方法都是调用AQS的final方法,没有什么不同之处。

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

基于Java+Springboot+Jpa+Mysql实现的在线网盘文件分享系统功能设计与实现二

一、前言介绍&#xff1a; 免费学习&#xff1a;猿来入此 1.1 项目摘要 在线网盘文件分享系统的课题背景主要源于现代社会对数字化信息存储和共享需求的日益增长。随着互联网的普及和技术的快速发展&#xff0c;人们越来越依赖电子设备来存储和传输各种类型的数据文件。然而…

DBSCAN聚类——基于密度的聚类算法(常用的聚类算法)

DBSCAN&#xff08;Density-Based Spatial Clustering of Applications with Noise&#xff09;简称密度聚类或密度基础聚类&#xff0c;是一种基于密度的聚类算法&#xff0c;也是一种常用的无监督学习算法&#xff0c;特别适用于形状不规则的聚类和含有噪声的数据集。主要用于…

C++:指针和引用

指针的基础 数据在内存当中是怎么样被存储的 数据在内存中的存储方式取决于数据的类型和计算机的体系结构 基本数据类型 整数类型&#xff1a;整数在内存中以二进制补码的形式存储。对于有符号整数&#xff0c;最高位为符号位&#xff0c;0 表示正数&#xff0c;1 表示负数。…

LabVIEW多通道面阵烟雾透过率测试系统

LabVIEW面阵烟雾透过率测试系统通过高精度多通道数据采集和实时处理技术&#xff0c;能够实现对固体推进剂烟雾的透过率进行精确测量。系统利用了LabVIEW的图形化编程环境及其丰富的设备驱动接口&#xff0c;有效提升了测试的自动化程度和数据处理的实时性。 项目背景&#xf…

PDF电子发票信息转excel信息汇总

PDF电子发票信息提取&#xff0c;支持将pdf发票文件夹下的剩所有发票&#xff0c;转为excel格式的信息&#xff0c;对于发票量比较大&#xff0c;不好统计&#xff0c;需要一个一个去统计的情况&#xff0c;可节省2个点以上的时间&#xff0c;一次下载&#xff0c;终身有效。 使…

51c视觉~合集7

我自己的原文哦~ https://blog.51cto.com/whaosoft/11536996 #Arc2Face 身份条件化的人脸生成基础模型&#xff0c;高一致性高质量的AI人脸艺术风格照生成 将人脸特征映射到SD的CLIP的编码空间&#xff0c;通过微调SD实现文本编码器转换为专门为将ArcFace嵌入投影到CLIP潜在…

【西瓜书】机器学习的模型评估

来源于西瓜书、南瓜书等内容。 误差与偏差 学习器的实际预测输出与样本的真实输出之间的差异&#xff0c;称为”误差“&#xff08;error&#xff09;。学习器在训练集上的误差&#xff0c;称为”训练误差“&#xff08;training error&#xff09;或”经验误差“&#xff08;…

Mac安装Docker Desktop搭建K8s集群,解决镜像无法下载的问题

使用 Docker Desktop可以在本地方便地搭建出 K8s集群&#xff0c;但开启 K8s集群后往往会遇到 K8s 镜像拉取失败问题&#xff0c;本文旨在解决该问题&#xff0c;从而在本地搭建 K8s 集群。 安装Docker Desktop 安装 Docker Desktop 建议安装历史版本, 不建议安装最新版。因为…

【Leecode】Leecode刷题之路第54天之旋转矩阵

题目出处 54-螺旋矩阵-题目出处 题目描述 个人解法 思路&#xff1a; todo代码示例&#xff1a;&#xff08;Java&#xff09; todo复杂度分析 todo官方解法 54-旋转矩阵-官方解法 方法1&#xff1a;模拟 思路&#xff1a; 代码示例&#xff1a;&#xff08;Java&#xff…

【YOLOv8】安卓端部署-1-项目介绍

【YOLOv8】安卓端部署-1-项目介绍 1 什么是YOLOv81.1 YOLOv8 的主要特性1.2 YOLOv8分割模型1.2.1 YOLACT实例分割算法之计算掩码1.2.1.1 YOLACT 的掩码原型与最终的掩码的关系1.2.1.2 插值时的目标检测中提取的物体特征1.2.1.3 coefficients&#xff08;系数&#xff09;作用1.…

Cesium教程01_实现Cartesian3 三维坐标操作

在 Vue 项目中使用 Cesium 实现 Cartesian3 三维坐标操作 目录 一、引言二、Cesium 与 Cartesian3 的优势三、示例应用&#xff1a;在地图上标注和计算距离 1. 项目结构2. 主要代码实现3. 运行与效果 四、代码讲解与扩展 1. Cartesian3 的基础操作2. 距离计算与中点标注 五、…

Qt5-雷达项目

界面: widget.h #ifndef WIDGET_H #define WIDGET_H#include <QTimer> #include <QWidget>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Widget(QWidget *parent nullptr);~Widget(); pr…

A040-基于springboot的智能停车计费系统设计与实现

&#x1f64a;作者简介&#xff1a;在校研究生&#xff0c;拥有计算机专业的研究生开发团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339; 赠送计算机毕业设计600…

数据结构初识

目录 1.初识 2.时间复杂度 常见时间复杂度举例&#xff1a; 3.空间复杂度 4.包装类&简单认识泛型 4.1装箱和拆箱 5.泛型 6.泛型的上界 7.泛型方法 8.List接口 1.初识 1.多画图 2.多思考 3.多写代码 4.多做题 牛客网-题库/在线编程/剑指offer 算法篇&#xff1a…

CUDA HOME does not exist, unable to compile CUDA op(s),已解决

有一个服务器上没有/usr/loacl/cuda&#xff0c;我也没有权限在这个目录装cuda&#xff0c;使用pip装完torch&#xff0c;llama factory使用时出现&#xff1a; 应该是本地没有nvcc相关执行文件。 先使用了&#xff1a; conda install -c cudatoolkit-dev不管用&#xff0c; …

杰发科技AC7801——ADC定时器触发的简单使用

使用场景 在需要多次采样结果的情况下&#xff0c;比如1s需要10w次的采样结果&#xff0c;可以考虑使用定时器触发采样&#xff0c;定时器设置多少的时间就会多久采样转换一次。 再加上使用dma&#xff0c;采样的结果直接放在dma的数组里面。 实现了自动采样&#xff0c;自动…

【有啥问啥】基于文本的图像检索(Text-Based Image Retrieval, TBIR)技术详解

基于文本的图像检索&#xff08;Text-Based Image Retrieval, TBIR&#xff09;技术详解 1. 背景理论知识 1.1 什么是基于文本的图像检索&#xff08;TBIR&#xff09;&#xff1f; 基于文本的图像检索&#xff08;Text-Based Image Retrieval&#xff0c;简称TBIR&#xff…

探索PyMuPDF:Python中的强大PDF处理库

文章目录 **探索PyMuPDF&#xff1a;Python中的强大PDF处理库**第一部分&#xff1a;背景第二部分&#xff1a;PyMuPDF是什么&#xff1f;第三部分&#xff1a;如何安装这个库&#xff1f;第四部分&#xff1a;至少5个简单的库函数使用方法第五部分&#xff1a;结合至少3个场景…

HarmonyOS Next 关于页面渲染的性能优化方案

HarmonyOS Next 关于页面渲染的性能优化方案 HarmonyOS Next 应用开发中&#xff0c;用户的使用体验至关重要。其中用户启动APP到呈现页面主要包含三个步骤&#xff1a; 框架初始化页面加载布局渲染 从页面加载到布局渲染中&#xff0c;主要包含了6个环节&#xff1a; 执行页…

已解决centos7 yum报错:cannot find a valid baseurl for repo:base/7/x86_64的解决方案

出现cannot find a valid baseurl for repo:base/7/x86_64错误通常是由于YUM仓库源无法找到或无法访问&#xff0c;导致YUM无法正常工作。这种情况常见于CentOS 7系统。解决这个问题需要检查几个方面&#xff0c;如网络连接、DNS设置和YUM仓库源配置。 &#x1f9d1; 博主简介&…