图解java.util.concurrent并发包源码系列——Condition条件等待队列深入详解

图解java.util.concurrent并发包源码系列——Condition条件等待队列深入详解

  • Condition的作用
  • Condition的原理
  • Condition源码
    • Condition的定义和Condition对象的获取
    • await方法
      • addConditionWaiter方法
        • unlinkCancelledWaiters方法
      • fullyRelease方法
      • isOnSyncQueue方法
      • checkInterruptWhileWaiting方法
      • reportInterruptAfterWait
    • signal方法
    • signalAll方法
  • 总结

往期文章:

  • 人人都能看懂的图解java.util.concurrent并发包源码系列 ThreadPoolExecutor线程池
  • 图解java.util.concurrent并发包源码系列,原子类、CAS、AtomicLong、AtomicStampedReference一套带走
  • 图解java.util.concurrent并发包源码系列——LongAdder
  • 图解java.util.concurrent并发包源码系列——深入理解AQS,看完可以吊打面试官
  • 图解java.util.concurrent并发包源码系列——深入理解ReentrantLock,看完可以吊打面试官
  • 图解java.util.concurrent并发包源码系列——深入理解ReentrantReadWriteLock读写锁,看完可以吊打面试官

Condition的作用

Condition是Java并发包提供的一个条件等待队列工具类,它具有让已获取到锁的线程当所需资源不满足的时候主动释放锁进入条件等待队列的能力,与Object的wait方法作用类似。

我们可以通过ReentrantLock的newCondition方法或者ReentrantReadWriteLock中WriteLock的newCondition方法获取Condition对象。

ReentrantLock与Condition的关系,相当于是synchronized关键字和Object#wait方法的关系。我们在调用Object的wait方法之前,必须先获取到synchronized锁的。相对应的在调用Condition的await方法前,必须要先获取到ReentrantLock的锁。

我们看一个生产者消费者的例子,了解Condition的具体作用。

/*** 生产者消费者例子* Created by huangjunyi on 2023/8/11.*/
public class ProviderConsumerDemo {private ReentrantLock reentrantLock;private Condition notEmpty;private Condition notFull;private LinkedList<Integer> queue;private int size;private int capacity;private Provider provider;private Consumer consumer;public ProviderConsumerDemo(int capacity) {this.reentrantLock = new ReentrantLock();this.notEmpty = reentrantLock.newCondition();this.notFull = reentrantLock.newCondition();this.queue = new LinkedList();this.capacity = capacity;this.size = 0;this.provider = new Provider();this.consumer = new Consumer();}class Provider {public void push(Integer num) {try {reentrantLock.lock();// 如果queue已经满了,生产者在notFull条件队列中等待while (size == capacity) notFull.await();queue.addLast(num);size++;// 唤醒在notEmpty条件队列中等待的消费者notEmpty.signal();} catch (Exception e) {e.printStackTrace();throw new RuntimeException("生产者发生异常");} finally {reentrantLock.unlock();}}}class Consumer {public Integer pull() {try {reentrantLock.lock();// 如果queue已经空了,消费者在notEmpty条件队列中等待while (size == 0) notEmpty.await();Integer num = queue.removeFirst();size--;// 唤醒在notFull条件队列中等待的生成者notFull.signal();return num;} catch (Exception e) {e.printStackTrace();throw new RuntimeException("消费者发生异常");} finally {reentrantLock.unlock();}}}public Provider provider() {return this.provider;}public Consumer consumer() {return this.consumer;}public static void main(String[] args) throws InterruptedException {ProviderConsumerDemo providerConsumerDemo = new ProviderConsumerDemo(5);Provider provider = providerConsumerDemo.provider();Consumer consumer = providerConsumerDemo.consumer();Thread providerThread = new Thread(() -> {int num = 0;for (int i = 0; i < 10000; i++) {provider.push(++num);}});providerThread.start();Thread consumerThread = new Thread(() -> {for (int i = 0; i < 10000; i++) {Integer num = consumer.pull();System.out.println(num);}});consumerThread.start();providerThread.join();consumerThread.join();}}

notEmpty和notFull是Condition类型的条件等待队列,通过调用ReentrantLock的newCondition()方法生成。

  • 当生产者想要往queue中放入元素时,发现queue的容量已经满了,那么就会调用notFull的await方法,在条件队列中进行等待。当生产者成功往queue中放入元素后,就会调用notEmpty的signal()方法唤醒notEmpty条件等待队列中的消费者。
  • 当消费者想要从queue中获取元素时,发现queue已经空了,那么就会调用notEmpty的await方法,在条件队列中进行等待。当消费者成功从queue中获取到元素后,就会调用notFull的signal()方法唤醒notFull条件等待队列中的生产者。

这样就实现了一个生成者消费者的功能。

在这里插入图片描述

Condition的原理

Condition其实就是一个用于存放因某种资源不充足而处于等待状态的线程的一个队列。比如上面的生产者线程等待queue队列的空间不满,好让它能够往queue中放入它要放的元素,那么就可以调用Condition的await方法把该生产者线程放入在Condition内部的队列中进行等待。

一个线程被转移到Condition时,会被封装为一个Node节点,放入到Condition内部的队列中,这与AQS的逻辑是相似的。不同点是AQS队列是双向链表,而Condition队列是单向链表。然后Node节点的waitStatus属性固定是CONDITION(-2)。

Condition条件等待队列有一个firstWaiter头指针和一个lastWaiter尾指针。

当前线程调用Condition的await方法时,必须是已经获取到锁的,然后它需要释放锁,释放锁的时候会唤醒AQS队列中的下一个节点,如当前线程没有获取到锁就调用Condition的await方法,在尝试释放锁时就会抛异常。

其他线程做完自己的操作之后,如果它自己的操作会使得某个Condition队列中的线程所等待的资源又充足时,可以调用这个Condition的signal方法唤醒Condition队列中的一个线程,或者调用Condition的signalAll方法唤醒Condition队列中的所有线程。

比如上面例子的消费者,由于它的消费,使得queue又有空间了,那么它可以唤醒在Condition队列等待queue有空间的生产者线程。

在这里插入图片描述

Condition源码

Condition的定义和Condition对象的获取

Condition接口:

public interface Condition {void await() throws InterruptedException;void awaitUninterruptibly();long awaitNanos(long nanosTimeout) throws InterruptedException;boolean await(long time, TimeUnit unit) throws InterruptedException;boolean awaitUntil(Date deadline) throws InterruptedException;void signal();void signalAll();
}

可以看到Condition接口除了普通的等待和唤醒方法,还提供了不响应中断(默认响应中断)和带超时机制的等待方法。

而在AQS的内部就定义了一个实现了Condition接口的内部类:

public class ConditionObject implements Condition, java.io.Serializable {private transient Node firstWaiter;private transient Node lastWaiter;
}

可以看到ConditionObject内部带了头指针firstWaiter和尾指针lastWaiter。

在这里插入图片描述

通过ReentrantLock的newCondition方法可以获取到ConditionObject对象。

ReentrantLock#newCondition:

    public Condition newCondition() {return sync.newCondition();}

ReentrantLock.Sync#newCondition:

        final ConditionObject newCondition() {return new ConditionObject();}

在这里插入图片描述

await方法

        public final void await() throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();// 封装为Node节点,放入Condition队列中Node node = addConditionWaiter();// 释放锁int savedState = fullyRelease(node);int interruptMode = 0;// 如果当前节点不在AQS同步队列中,那么就一直循环parkwhile (!isOnSyncQueue(node)) {// park挂起当前线程LockSupport.park(this);// 唤醒后检查是否被中断,记录一下中断标记interruptMode,方便后续处理if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}// 在AQS的同步队列等待重新获取锁,如果在此期间再次被中断,则次记录标记interruptModeif (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;// 清除条件等待队列中被取消的节点if (node.nextWaiter != null)unlinkCancelledWaiters();// 对中断的统一处理if (interruptMode != 0)reportInterruptAfterWait(interruptMode);}
  1. 首先要把当前线程封装成一个Node,然后把该Node放入到Condition的条件队列中。
  2. Node入队列后,当前线程要释放所有获取的锁。
  3. 释放完所有的锁后,就一直while循环检查当前线程对应的节点是否已经被挪到了AQS的同步队列当中,如果已经挪入了(其他线程调用Condition的signal方法会把Condition队列中的一个节点挪到AQS同步队列中),那么跳出循环,否则就把当前线程挂起。每次线程醒来时,都要检查一下自己是否被中断了,如果是,要记录一个中断标记interruptMode,方便后续处理。
  4. 节点被挪入到AQS同步队列后,当前线程就要等待重新获取锁。获取到锁后再检查一下自己是否被中断了,如果是,则更新一下中断标记。
  5. 获取到锁后,会尝试清除一遍条件队列中已被中断的节点。
  6. 最后对中断的情况进行处理

在这里插入图片描述

addConditionWaiter方法

addConditionWaiter方法的作用是往条件等待队列添加一个节点。

        private Node addConditionWaiter() {Node t = lastWaiter;// 如果尾节点不是CONDITION状态的,那么清理一遍队列,然后再次获取尾节点if (t != null && t.waitStatus != Node.CONDITION) {unlinkCancelledWaiters();t = lastWaiter;}// 创建一个新节点,waitStatus状态值为CONDITIONNode node = new Node(Thread.currentThread(), Node.CONDITION);// t尾节点为null,表示队列为空,当前节点作为头节点if (t == null)firstWaiter = node;// 队列不为空,就入队列尾部elset.nextWaiter = node;lastWaiter = node;return node;}

addConditionWaiter方法的大体思路就是拿到一个waitStatus状态值为CONDITION的节点,或者null,然后把当前线程封装为一个Node节点,waitStatus设置为CONDITION,然后放入队列尾部(队列不为空)或者设置为队列头节点(队列为空)。

在这里插入图片描述

unlinkCancelledWaiters方法

unlinkCancelledWaiters()方法的作用是清理条件等待队列。每个进入条件等待队列中的节点的waitStatus属性原本都是CONDITION,但是随着队列中某些节点的线程被中断等待,它的waitStatus属性就不是CONDITION了,那么这些waitStatus属性不是CONDITION的节点,是不需要的,自然要清理出队列。

        private void unlinkCancelledWaiters() {// t指针,指向后面一个节点Node t = firstWaiter;// trail指针,指向前面一个节点Node trail = null;while (t != null) {Node next = t.nextWaiter;// t指针发现waitStatus不为CONDITION的节点if (t.waitStatus != Node.CONDITION) {t.nextWaiter = null;if (trail == null)// 要断连的节点是头节点,那么更新头节点为t指针指向的节点的下一个节点firstWaiter = next;else// trail指针指向的节点的nextWaiter指针,指向t指针指向的节点的下一个节点trail.nextWaiter = next;if (next == null)// 没有后续节点了,把trail指针当前所指的节点设置为尾节点lastWaiter = trail;}elsetrail = t;t = next;}}

两个指针一前一后地遍历队列,t是后一个节点的指针,trail是前一个节点的指针。当t指针遇到waitStatus属性不为CONDITION的节点的时候,就把trail节点指向t节点的下一个节点(next),这样t指针指向的节点自然断连出队列,后续会被GC回收。而如果要断连的节点刚好是头节点,那么就要更新头节点为要断连的节点的下一个节点。

在这里插入图片描述

fullyRelease方法

fullyRelease方法用于进入了条件等待队列的节点对应的线程释放锁资源。

    final int fullyRelease(Node node) {boolean failed = true;try {int savedState = getState();// 调用AQS的release方法一口气全部释放if (release(savedState)) {failed = false;// 释放了多少,返回多少,后面会再次获取return savedState;} else {throw new IllegalMonitorStateException();}} finally {if (failed)node.waitStatus = Node.CANCELLED;}}

int savedState = getState(); 获取state遍历,然后再调用 release(savedState) 一口气把自己获取的所有锁资源全部释放。然后释放了多少,方法的返回值就是多少,后续重新获取锁时就获取多少。

在这里插入图片描述

isOnSyncQueue方法

isOnSyncQueue方法是用于判断当前node节点是否在AQS同步队列中,

    final boolean isOnSyncQueue(Node node) {// waitStatus 属性还是CONDITION ,或者prev指针是空,代表不在AQS同步队列中,因为AQS同步队列中的节点是没有CONDITION 状态的,而且AQS队列是一个双向链表,prev是不会为null的if (node.waitStatus == Node.CONDITION || node.prev == null)return false;// node的next指针不为null,那么node肯定再AQS队列中,因为只有再AQS同步队列中,才会用next指针指向下一个节点,在Condition条件队列中用的是nextWaiter指针if (node.next != null) return true;// node的prev指针不为null,也不代表当前节点就在AQS队列中,因为把一个节点放入AQS同步队列是通过CAS完成的,但是CAS有可能会失败,所以这里调用findNodeFromTail方法从尾部开始寻找,确保node节点确实已经在AQS同步队列中了。return findNodeFromTail(node);}

首先判断当前节点如果状态是CONDITION ,那么肯定不在AQS同步队列中,因为AQS同步队列中的节点是没有CONDITION 状态的。
如果当前节点的prev指针是null,那么也不可能在AQS队列中。因为AQS同步队列是一个双向链表,而node节点是从链表尾部进去的,如果它再AQS同步队列中,prev指针是不可能为null的。
如果node节点的next指针不为null,那么肯定再AQS同步队列中。因为只有AQS同步队列中的节点才会用next指针记录下一个节点,在Condition条件等待队列中的节点是用nextWaiter指针指向下一个节点的。
当这里还不能确定当前节点已经在AQS队列中,即使当前节点的prev指针不为null,但是一个节点进入AQS队列是通过CAS放进去的,而CAS是有可能失败的,所以还要调findNodeFromTail方法从尾部开始寻找,确保当前节点已经进入了AQS同步队列。

    private boolean findNodeFromTail(Node node) {Node t = tail;for (;;) {if (t == node)// 找到了,返回truereturn true;if (t == null)// 没有了,返回falsereturn false;// 通过prev指针网球遍历t = t.prev;}}

findNodeFromTail方法就是从尾部开始,通过prev指针往前遍历,直到找到为止或者遍历完毕。

在这里插入图片描述

checkInterruptWhileWaiting方法

checkInterruptWhileWaiting方法用于检查当前线程是否被中断,也就是取消等待。如果当前线程被中断了,要设置对应的标志,方便后续处理。

        private int checkInterruptWhileWaiting(Node node) {return Thread.interrupted() ?(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :0;}

Thread.interrupted()获取当前线程中断标志位,这个方法被调用后,当前线程的Thread对象的中断标志位就会被复位。

transferAfterCancelledWait(node):

    final boolean transferAfterCancelledWait(Node node) {// compareAndSetWaitStatus尝试把当前节点的waitStatus状态改为0,// 如果修改成功,表示当前节点处于Condition条件等待队列中被中断。// 如果CAS不成功,表示当前节点的waitStatus状态不为CONDITION,当前节点不在Condition队列中,也不是在条件队列中被中断,那么当前节点就是在signal之后被中断。if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {enq(node);return true;}// 到这里,说明当前节点node,是在signal之后被中断的,确保节点已经在AQS队列中之后,返回false。while (!isOnSyncQueue(node))Thread.yield();return false;}

这里中断分两种情况,一种当前节点还是CONDITION状态,在Condition队列中被中断;另一种情况就是被其他线程调用了signal方法,修改了当前node节点的waitStatus,然后当前node节点才被中断。

分这两种情况是因为后面对于这两种情况的处理是不同的,如果是在Condition队列中被中断的,那么后续要抛出中断异常。如果是被别的线程调用了signal方法,修改了当前node节点的waitStatus后,才中断当前线程,由于这里通过Thread.interrupted()检查当前线程是否被中断时,把当前线程的中断标志位复位了,这里只需要把中断标志位重新置位即可。

在这里插入图片描述

reportInterruptAfterWait

        private void reportInterruptAfterWait(int interruptMode)throws InterruptedException {// 在Condition条件队列中被打断的,抛异常if (interruptMode == THROW_IE)throw new InterruptedException();// 被别的线程signal之后才被打断的,中断标志位重新置位else if (interruptMode == REINTERRUPT)selfInterrupt();}static void selfInterrupt() {Thread.currentThread().interrupt();}
  1. 如果是在Condition条件队列中被打断的,那么前面记录的中断标记interruptMode就是THROW_IE,这里要抛出异常。
  2. 如果是被别的线程signal之后才被打断的,中断标志位重新置位即可。
  3. 两个分支没进,那么就是没有被中断。

在这里插入图片描述

signal方法

signal方法用于唤醒等待在Condition条件队列中的线程。

        public final void signal() {// 非持有锁的线程,调signal方法会抛出异常if (!isHeldExclusively())throw new IllegalMonitorStateException();Node first = firstWaiter;if (first != null)doSignal(first);}

首先判断如果当前线程不是持有锁的线程,那么会抛出一个异常。如果当前线程是持有锁的线程,那么会调用doSignal方法。

        private void doSignal(Node first) {do {if ( (firstWaiter = first.nextWaiter) == null)lastWaiter = null;first.nextWaiter = null;// 调用transferForSignal(first)方法转移当前节点到AQS同步队列中,成功转移一次就退出循环} while (!transferForSignal(first) &&(first = firstWaiter) != null);}

doSignal方法里面就是拿到Condition条件队列中的头节点,转移到AQS同步队列中,转移成功就退出循环,方法结束。

    final boolean transferForSignal(Node node) {// 转移前,先修改节点状态if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))return false;// 节点如AQS同步队列Node p = enq(node);// enq(node)会返回node入队后的前驱节点,// 如果前驱节点的waitStatus属性是CANCELLED状态,或者CAS修改前驱节点waitStatus属性不成功,那么就直接唤醒node节点的线程,// 否则node节点的线程将会由AQS中该节点的前驱节点的线程释放锁后唤醒int ws = p.waitStatus;if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))LockSupport.unpark(node.thread);return true;}

在这里插入图片描述

signalAll方法

signalAll方法与signal方法功能相同,区别是signal方法只转一个节点到AQS队列中,而signalAll方法则是转移所有节点到AQS队列中。

        public final void signalAll() {// 当前线程没有获取锁,抛异常if (!isHeldExclusively())throw new IllegalMonitorStateException();Node first = firstWaiter;if (first != null)doSignalAll(first);}

如果发现当选线程没有获取到锁就调用了signalAll方法,那么也是抛一个异常。否则就调用doSignalAll方法。

        private void doSignalAll(Node first) {lastWaiter = firstWaiter = null;do {Node next = first.nextWaiter;first.nextWaiter = null;transferForSignal(first);first = next;} while (first != null);}

可以看到也是一个do-while循环,但是这个循环不是转移成功一个节点就退出,而是直到没有节点可以转移为止。

在这里插入图片描述

总结

Condition的源码到此就分析完毕了,Condition的核心逻辑就是把调用了await方法的线程,封装为Node节点放入到条件队列中,释放掉该线程获取的所有锁资源,然后挂起,等待其他线程调用signal方法或者signalAll方法把他移入AQS同步队列中然后将其唤醒。

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Windows10下启动RocketMQ报错:找不到或无法加载主类 Files\Java\jdk1.8.0_301\lib\dt.jar】解决方法

Windows10下启动RocketMQ报错&#xff1a;找不到或无法加载主类 一、问题产生二、产生原因三、解决办法 一、问题产生 参考RocketMQ Github官网上的说明&#xff0c;下载rocketmq-all-5.1.3-bin-release.zip&#xff0c;解压配置环境变量后&#xff0c;执行如下命令&#xff1a…

竞赛项目 深度学习手势识别算法实现 - opencv python

文章目录 1 前言2 项目背景3 任务描述4 环境搭配5 项目实现5.1 准备数据5.2 构建网络5.3 开始训练5.4 模型评估 6 识别效果7 最后 1 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 深度学习手势识别算法实现 - opencv python 该项目较为新颖…

基于Flask的模型部署

基于Flask的模型部署 一、背景 Flask&#xff1a;一个使用Python编写的轻量级Web应用程序框架&#xff1b; 首先需要明确模型部署的两种方式&#xff1a;在线和离线&#xff1b; 在线&#xff1a;就是将模型部署到类似于服务器上&#xff0c;调用需要通过网络传输数据&…

【数据结构】反转链表、链表的中间节点、链表的回文结构(单链表OJ题)

正如标题所说&#xff0c;本文会图文详细解析三道单链表OJ题&#xff0c;分别为&#xff1a; 反转链表 &#xff08;简单&#xff09; 链表的中间节点 &#xff08;简单&#xff09; 链表的回文结构 &#xff08;较难&#xff09; 把他们放在一起讲的原因是&#xff1a; 反转链…

A2C原理和代码实现

参考王树森《深度强化学习》课程和书籍 1、A2C原理&#xff1a; Observe a transition&#xff1a; ( s t , a t , r t , s t 1 ) (s_t,{a_t},r_t,s_{t1}) (st​,at​,rt​,st1​) TD target: y t r t γ ⋅ v ( s t 1 ; w ) . y_{t} r_{t}\gamma\cdot v(s_{t1};\mathbf…

【RabbitMQ】golang客户端教程5——使用topic交换器

topic交换器&#xff08;主题交换器&#xff09; 发送到topic交换器的消息不能具有随意的routing_key——它必须是单词列表&#xff0c;以点分隔。这些词可以是任何东西&#xff0c;但通常它们指定与消息相关的某些功能。一些有效的routing_key示例&#xff1a;“stock.usd.ny…

知识图谱基本工具Neo4j使用笔记 四 :使用csv文件批量导入图谱数据

文章目录 一、系统说明二、说明三、简单介绍1. 相关代码以及参数2. 简单示例 四、实际数据实践1. 前期准备&#xff08;1&#xff09; 创建一个用于测试的neo4j数据库&#xff08;2&#xff09;启动neo4j 查看数据库 2. 实践&#xff08;1&#xff09; OK 上面完成后&#xff0…

阿里云Linux服务器安装FTP站点全流程

阿里云百科分享使用阿里云服务器安装FTP全教程&#xff0c;vsftpd&#xff08;very secure FTP daemon&#xff09;是Linux下的一款小巧轻快、安全易用的FTP服务器软件。本教程介绍如何在Linux实例上安装并配置vsftpd。 目录 前提条件 步骤一&#xff1a;安装vsftpd 步骤二…

HTTP代理编程:Python实用技巧与代码实例

今天我要与大家分享一些关于HTTP代理编程的实用技巧和Python代码实例。作为一名HTTP代理产品供应商&#xff0c;希望通过这篇文章&#xff0c;帮助你们掌握一些高效且实用的编程技巧&#xff0c;提高开发和使用HTTP代理产品的能力。 一、使用Python的requests库发送HTTP请求&a…

无涯教程-Perl - mkdir函数

描述 此功能使用MODE指定的模式创建一个名称和路径EXPR的目录,为清楚起见,应将其作为八进制值提供。 语法 以下是此函数的简单语法- mkdir EXPR,MODE返回值 如果失败,此函数返回0,如果成功,则返回1。 例 以下是显示其基本用法的示例代码- #!/usr/bin/perl -w$dirname &…

Docker desktop使用配置

1. 下载安装 https://www.docker.com/ 官网下载并安装doker desktop 2. 配置镜像 &#xff08;1&#xff09;首先去阿里云网站上进行注册&#xff1a;https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors &#xff08;2&#xff09;注册完成后搜索&#xff1a;容…

Jmeter入门之digest函数 jmeter字符串连接与登录串加密应用

登录请求中加密串是由多个子串连接&#xff0c;再加密之后传输。 参数连接&#xff1a;${var1}${var2}${var3} 加密函数&#xff1a;__digest &#xff08;函数助手里如果没有该函数&#xff0c;请下载最新版本的jmeter5.0&#xff09; 函数助手&#xff1a;Options > …

1.Fay-UE5数字人工程导入(UE数字人系统教程)

非常全面的数字人解决方案(含源码) Fay-UE5数字人工程导入 1、工程下载&#xff1a;xszyou/fay-ue5: 可对接fay数字人的ue5工程 (github.com) 2、ue5下载安装&#xff1a;Unreal Engine 5 3、ue5插件安装 依次安装以下几个插件 4、双击运行工程 5、切换中文 6、检…

JavaWeb学习|JavaBean;MVC三层架构;Filter;Listener

1.JavaBean 实体类 JavaBean有特定的写法: 必须要有一个无参构造 属性必须私有化。 必须有对应的get/set方法 用来和数据库的字段做映射 ORM; ORM:对象关系映射 表--->类 字段-->属性 行记录---->对象 2.<jsp&#xff1a;useBean 标签 3. MVC三层架构 4. Filter …

Mybatis 初识

目录 1. MyBatis入门 1.1 MyBatis的定义 1.2 MyBatis的核心 MyBatis的核心 JDBC 的操作回顾 1.3 MyBatis的执行流程 MyBatis基本工作原理 2. MyBatis的使用 2.1 MyBatis环境搭建 2.1.1 创建数据库和表 2.1.2 添加MyBatis框架支持 老项目添加MyBatis 新项目添加MyBatis 2.1.3 设…

考研算法38天:反序输出 【字符串的翻转】

题目 题目收获 很简单的一道题&#xff0c;但是还是有收获的&#xff0c;我发现我连scanf的字符串输入都忘记咋用了。。。。。我一开始写的 #include <iostream> #include <cstring> using namespace std;void deserve(string &str){int n str.size();int…

css小练习:案例6.炫彩加载

一.效果浏览图 二.实现思路 html部分 HTML 写了一个加载动画效果&#xff0c;使用了一个包含多个 <span> 元素的 <div> 元素&#xff0c;并为每个 <span> 元素设置了一个自定义属性 --i。 这段代码创建了一个简单的动态加载动画&#xff0c;由20个垂直排列的…

Flask实现接口mock,安装及使用教程(一)

1、什么是接口mock 主要是针对单元测试的应用&#xff0c;它可以很方便的解除单元测试中各种依赖&#xff0c;大大的降低了编写单元测试的难度 2、什么是mock server 正常情况下&#xff1a;测试客户端——测试——> 被测系统 ——依赖——>外部服务依赖 在被测系统和…

AI:01-基于机器学习的深度学习的玫瑰花种类的识别

文章目录 一、数据集介绍二、数据预处理三、模型构建四、模型训练五、模型评估六、模型训练七、模型评估八、总结深度学习技术在图像识别领域有着广泛的应用,其中一种应用就是玫瑰花种类的识别。在本文中,我们将介绍如何使用机器学习和深度学习技术来实现玫瑰花种类的识别,并…

运维监控学习1

1、监控对象&#xff1a; 1、监控对象的理解&#xff1b;CPU是怎么工作的&#xff1b; 2、监控对象的指标&#xff1a;CPU使用率&#xff1b;上下文切换&#xff1b; 3、确定性能基准线&#xff1a;CPU负载多少才算高&#xff1b; 2、监控范围&#xff1a; 1、硬件监控&#x…