JAVA 异步编程(异步,线程,线程池)一

news2024/9/21 12:24:40

目录

1.概念

1.1 线程和进程的区别

1.2 线程的五种状态

1.3 单线程,多线程,线程池

1.4 异步与多线程的概念

2. 实现异步的方式

2.1 方式1 裸线程(Thread)

2.1 方式2 线程池(Executor)

2.1.1 源码分析

2.1.2  线程池创建(Executors)

 2.1.3 阻塞主线程获取子线程返回值

1.线程池awaitTermination轮询

2.线程池future.get

3. CountDownLatch类的await(推荐)

4.线程池invokeAll 

5. ExecutorCompletionService(强烈推荐)

2.1.4 非阻塞主线程获取子线程返回值

1. Future接口:

2. CompletableFuture类:

2.3 方式3 ForkJoinPool

2.4 方式4 Spring的Async注解

4. 总结:


1.概念

1.1 线程和进程的区别

        进程:是一个动态的过程,是一个活动的实体,简单来说,一个应用程序的运行就可以看作是一个进程。可以说,进程中包含了多个可以同时运行的线程。

线程(Thread):是运行中实际任务的执行者,线程是操作系统能够进行运算调度的最小单位。它被包装在进程中,是进程中的实际运行单位。每个线程都有自己的程序计数器、堆栈和局部变量,但它们共享进程的代码和内存。

1.2 线程的五种状态

  1. 新建(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new Thread()

  2. 就绪(Runnable):也被称为“可执行”状态,当线程对象调用start()方法(启动线程)后,线程即进入就绪状态。处于就绪状态的线程,只是说明了该线程可以运行,但还没有真正运行,等待CPU分配时间片。

  3. 运行(Running):当CPU开始调度处于就绪状态的线程时,线程进入运行状态,真正开始执行线程代码。

  4. 阻塞(Blocked):线程在运行过程中可能因为各种原因导致无法继续执行,比如等待I/O操作结果,或者尝试获得一个同步监视器而失败,这时它就会进入阻塞状态。

  5. 死亡(Dead):线程执行完毕或者因异常退出run()方法后,线程就进入死亡状态。

1.3 单线程,多线程,线程池

单线程: 顾名思义是只有一条线程在执行任务,在工作中很难遇到。

多线程: 是创建多条线程同时执行任务。

线程池(ThreadPool): 是一种管理线程的机制,它能够复用线程,避免因频繁创建和销毁线程导致的性能问题。通过设置线程池的大小,可以有效管理线程的运行。

1.4 异步与多线程的概念

        异步和多线程并不是同一关系,异步是最终目的,多线程只是我们实现异步的一种手段。异步是调用者发送一个请求给被调用者,而调用者不用等待请求结果的返回,可以去做其他事。实现异步可以使用多线程或交给其他进程来处理。

2. 实现异步的方式

Util类:

public class Util {

    /**
     * sleep
     *
     * @param milliseconds
     */
    public static void mySleep(int milliseconds) {
        try {
            TimeUnit.MILLISECONDS.sleep(milliseconds);
        } catch (InterruptedException e) {

        }
    }

    /**
     * print log
     * @param message
     */
    public static void printfLog(String message) {
        LocalDateTime localDateTime = LocalDateTime.now();
        String dateString = localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
        System.out.println(String.format("%s - %s", dateString, message));
    }
}

2.1 方式1 裸线程(Thread)

        使用“原汁原味”的裸线程(Thread)。Java线程本质上被映射到操作系统线程,并且每个线程对象对应着一个计算机底层线程。

JVM管理着线程的生存期,而且只要你不需要线程间通讯,你也不需要关注线程调度。

每个线程有自己的栈空间,它占用了JVM进程空间的指定一部分。

线程的接口相当简明,你只需要提供一个Runnable,调用.start()开始计算。没有现成的API来结束线程,你需要自己来实现,通过类似boolean类型的标记来通讯。

    private static void threadMethod(List<Integer> list) {
        while (list.get(0) > 0) {
            Util.mySleep(Double.valueOf(Math.random() * 100).intValue());
            Integer result = list.get(0);
            result--;
            list.add(0, result);
        }
    }
    public static void threadTest() {
        Util.printfLog("主线程开始");
        //线程外的变量只读,值类型只能显示的生命final
        //这里正常应该会输出list result=0,但是这里因为时多个线程同时操作一个变量导致线程不安全,输出list result为负值
        List<Integer> list = new ArrayList<>(1);
        list.add(100);
        for (int i = 0; i < 100; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    threadMethod(list);
                    Util.printfLog("子线程结束!currentName = " + Thread.currentThread().getName());
                }
            };
            Thread thread = new Thread(runnable);
            thread.start();
        }
        Util.printfLog("主线程结束");
        Util.mySleep(10000);
        Util.printfLog("list result=" + list.get(0));
    }

这里暂时没考虑线程安全行,所以list result可能会出现负值。

2.1 方式2 线程池(Executor)

       2.1.1 源码分析

 Executor一个接口,它通过一系列抽象类,接口等最终生成了线程池ThreadPoolExecutor。

它的包在java.util.concurrent。线程池的底层实现也是通过一系列的操作,通过Thread创建单独的线程。

ExecutorService.submit->AbstractExecutorService.submit->ThreadPoolExecutor.execute->
ThreadPoolExecutor.addWorker->Worker构造函数->DefaultThreadFactory.newThread

也可以使用工具类Executors创建线程池,底层也是通过创建ThreadPoolExecutor创建线程池。

ThreadPoolExecutor的主要方法:

方法名描述
submit

Callable<T>的实现类,创建带返回值的线程

Runnable的实现类,创建不带返回值的线程

shutdown优雅的终止线程。线程池中的线程不会立即结束,等线程池中没有在运行的线程才终止后台的线程池。
shutdownNow直接终止线程。不管线程池中有没有在运行的线程,直接将后台的线程池终止。

2.1.2  线程池创建(Executors)

线程池Executors工具类的主要方法:

方法名描述
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。

ScheduledExecutorService的方法:

        schedule:定时到达时间间隔执行。
        scheduleAtFixedRate:周期,从上一个任务开始运行就开始计时
        scheduleWithFixedDelay:周期,从上一次任务运行结束开始计时

    public static void main(String[] args) throws IOException {
        System.out.println("主线程【开始】");
        executorsNoWait();
        System.out.println("主线程【结束】");
        System.in.read();
}    

/**
     * 线程直接提交,提交完成之后直接直接回到主进程
     */
    private static void executorsNoWait() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        executorService.submit(() -> {
            threadMethod(5, "线程--1");
            return "第一线程";
        });
        executorService.submit(() -> {
            threadMethod(2, "线程--2");
            return "第二线程";
        });
        // 线程池没shutdwon,后台运行
        executorService.shutdown();
    }


private static void threadMethod(int num, String name) {
        for (int i = 0; i < num; i++) {
            Util.mySleep(1000);
            Util.printfLog(String.format("%s", name));
        }
    }

这种方式创建只提交线程到线程池,不阻塞主线程,不获取线程的返回值。通过代码运行结果,主线程已经执行完成,但是线程池中的子线程还没结束所以没有获取结果就不会阻塞主线程的运行。 只有当线程池中的没有runing的线程,线程池才会shutdown。

 2.1.3 阻塞主线程获取子线程返回值

        这种方式就是主线程提交子线程后,子线程异步运行。然后通过阻塞主线程,等待子线程都运行结束,再获取子线程的结果

1.线程池awaitTermination轮询

这种方式只能通过future.get方式获取返回值,只能等全部子线程执行完成才能获取,通过future.get返回值。

  /**
     * 线程池等待,阻塞回到主进程--awaitTermination轮训
     */
    private static void executorsAwaitTermination() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Future<String> future = executorService.submit(() -> {
            threadMethod(5, "线程--1");
            return "第一线程";
        });
        Future<String> future2 = executorService.submit(() -> {
            threadMethod(2, "线程--2");
            return "第二线程";
        });
        executorService.shutdown();
        try {
            while (!executorService.awaitTermination(1, TimeUnit.SECONDS)) {
                Util.printfLog("等待中");
            }
        } catch (Exception e) {
        }
    }

2.线程池future.get

直接使用future.get获取子线程的返回值,通过遍历集合中的future,以固定的顺序获取子线程返回值,只有当获取的子线程有返回值之后才继续循环获取下一个子线程的返回值。

 /**
     * 线程池等待,阻塞回到主进程--Future.get()
     */
    private static void executorsFutureGet() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        List<Future<String>> listTemp = new ArrayList<>();
        Future<String> future = executorService.submit(() -> {
            threadMethod(5, "线程--1");
            return "第一线程";
        });
        listTemp.add(future);
        Future<String> future2 = executorService.submit(() -> {
            threadMethod(2, "线程--2");
            return "第二线程";
        });
        listTemp.add(future2);
        for (Future<String> item : listTemp) {
            try {
                System.out.println(item.get());
            } catch (Exception e) {
            }
        }
        executorService.shutdown();
    }

3. CountDownLatch类的await(推荐)

通过await方法阻塞主线程等待全部子线程执行完成后,通过future.get获取子线程的返回值,这种方式比方式一更加优雅。

    /**
     * 线程池等待,阻塞回到主进程--CountDownLatch
     */
    private static void executorsCountDownLatch() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        CountDownLatch countDownLatch = new CountDownLatch(2);
        Future<String> future = executorService.submit(() -> {
            threadMethod(5, "线程--1");
            countDownLatch.countDown();
            return "第一线程";
        });
        Future<String> future2 = executorService.submit(() -> {
            threadMethod(2, "线程--2");
            countDownLatch.countDown();
            return "第二线程";
        });
        executorService.shutdown();
        try {
            countDownLatch.await();
            Util.printfLog(future2.get());
            Util.printfLog(future.get());
        } catch (Exception e) {

        }
    }

4.线程池invokeAll 

这种方式和方式二类似,也是通过future.get获取子线程返回值,只能通过遍历集合中的future,以固定的顺序获取子线程返回值。

/**
     * 线程池等待,阻塞回到主进程---使用invokeAll
     */
    private static void executorsInvokeAll() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Callable<String> callable = () -> {
            threadMethod(5, "线程--1");
            return "第一线程";
        };
        Callable<String> callable2 = () -> {
            threadMethod(2, "线程--2");
            return "第二线程";
        };
        List<Callable<String>> list = Arrays.asList(callable, callable2);
        try {
            List<Future<String>> futureList = executorService.invokeAll(list);
            executorService.shutdown();
            Util.printfLog("获取结果中---");
            for (Future<String> item : futureList) {
                Util.printfLog(item.get());
            }
        } catch (Exception e) {
        }
    }

5. ExecutorCompletionService(强烈推荐)
  • 优雅的获取子线程返回值,只要任何子线程结束就有返回值。
  • ExecutorCompletionService内部管理者一个已完成任务的阻塞队列
  • ExecutorCompletionService引用了一个Executor, 用来执行任务
  • submit()方法最终会委托给内部的executor去执行任务
  • take/poll方法的工作都委托给内部的已完成任务阻塞队列
  • 如果阻塞队列中有已完成的任务, take方法就返回任务的结果, 否则阻塞等待任务完成。
/**
     * 线程池等待,阻塞回到主进程---使用ExecutorCompletionService
     */
    private static void executorsExecutorCompletionService() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        ExecutorCompletionService<String> executorCompletionService = new ExecutorCompletionService(executorService);

        executorCompletionService.submit(() -> {
            threadMethod(5, "线程--1");
            return "第一线程返回";
        });
        executorCompletionService.submit(() -> {
            threadMethod(2, "线程--2");
            return "第二线程返回";
        });
        executorService.shutdown();
        try {
            Util.printfLog(executorCompletionService.take().get());
            Util.printfLog(executorCompletionService.take().get());
        } catch (Exception e) {
        }

    }

2.1.4 非阻塞主线程获取子线程返回值

1. Future接口:

        JDK 5引入了Future模式。Future接口是Java多线程Future模式的实现,在java.util.concurrent包中,可以来进行异步计算。Future模式是多线程设计常用的一种设计模式。
        Future虽然可以实现获取异步执行结果的需求,但它没有提供通知的机制,我们无法得知Future什么时候完成,不是真正意义上的异步。
        使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,这两种方式都会使主线程也会被迫等待,耗费CPU的资源。

private static void futureGet() {
        try {
            ExecutorService executor = Executors.newFixedThreadPool(1);
            Future<Integer> future = executor.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {

                    Util.printfLog("===task start===");
                    Util.mySleep(5000);
                    Util.printfLog("===task finish===");
                    return 3;
                }
            });
            executor.shutdown();
            //这里需要返回值时会阻塞主线程
            Integer result = future.get();
            Util.printfLog("线程返回值:" + result);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

 

2. CompletableFuture类:

        CompletableFuture实现异步操作,加上对lambda的支持,可以说实现异步任务已经发挥到了极致。CompletableFuture弥补了Future模式的缺点。在异步的任务完成后,需要用其结果继续操作时,无需等待。可以直接通过thenAccept、thenApply、thenCompose等方式将前面异步处理的结果交给另外一个异步事件处理线程来处理。

CompletableFuture的静态工厂方法:

方法名描述
runAsync(Runnable runnable)使用ForkJoinPool.commonPool()作为它的线程池执行异步代码,异步操作无返回值
runAsync(Runnable runnable, Executor executor)使用指定的thread pool执行异步代码,异步操作无返回值
supplyAsync(Supplier<U> supplier)使用ForkJoinPool.commonPool()作为它的线程池执行异步代码,异步操作有返回值
supplyAsync(Supplier<U> supplier, Executor executor)使用指定的thread pool执行异步代码,异步操作有返回值
 /**
     * 线程提交后,不阻塞主进程,
     * completableFuture子线程执行完后回调--thread pool执行异步代码
     */
    private static void completableFutureThreadPool() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
            threadMethod(5, "线程--1");
            return "第一线程";
        }, executorService);
        CompletableFuture<String> completableFuture2 = CompletableFuture.supplyAsync(() -> {
            threadMethod(2, "线程--2");
            return "第二线程";
        }, executorService);
        executorService.shutdown();
        completableFuture.thenAccept((r) -> {
            Util.printfLog(r);
        });
        completableFuture2.thenAccept((r) -> {
            Util.printfLog(r);
        });
    }

2.3 方式3 ForkJoinPool

        Java 8中加入了并行流,从此我们有了一个并行处理集合的简单方法。它和lambda一起,构成了并发计算的一个强大工具。默认情况下是通过ForkJoinPool.commonPool()实现并行的。这个通用池由JVM来管理,并且被JVM进程内的所有线程共享。

    /**
     * 使用ForkJoinPool,异步发处理
     */
    private static void forkJoinPool() {
        List<Supplier<String>> actionList = Arrays.asList(() -> {
                    threadMethod(5, "线程--1");
                    return "第一线程";
                },
                () -> {
                    threadMethod(2, "线程--2");
                    return "线程2返回";
                });
        List<String> threadResult = actionList.parallelStream().map(row -> row.get()).collect(Collectors.toList());
        Util.printfLog(threadResult.stream().collect(Collectors.joining(",")));
    }

2.4 方式4 Spring的Async注解

spring实现异步需要开启注解@EnableAsync,可以使用xml方式或者java code config的方式。
 (1)@Async 异步的方法

4. 总结:

        虽然Thread可以创建线程,但是线程的创建销毁不能很好的控制,就会导致资源耗尽的风险,所以线程资源尽量通过线程池提供,不在应用中自行显示的创建线程,一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。

        线程池的创建尽量不使用Executors,而要通过ThreadPoolExecutor方式,这一方面是由于jdk中Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活;另外由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。

参考:

Java 并发的四种风味:Thread、Executor、ForkJoin 和 Actor

java多线程并发之旅-28-Executor CompletionService ExecutorCompletionService 详解

Java8新的异步编程方式 CompletableFuture

java线程池ThreadPoolExecutor类使用详解

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

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

相关文章

三丰云评测:免费虚拟主机与免费云服务器体验

今天我来为大家分享一下我对三丰云的评测。作为一家知名的云服务提供商&#xff0c;三丰云一直以来备受用户好评。他们提供免费虚拟主机和免费云服务器服务&#xff0c;深受网站建设者和开发者的喜爱。 首先谈谈免费虚拟主机服务。三丰云的免费虚拟主机方案性价比非常高&#x…

代码随想录算法训练营第33天|LeetCode 509. 斐波那契数、70. 爬楼梯、746. 使用最小花费爬楼梯

1. LeetCode 509. 斐波那契数 题目链接&#xff1a;https://leetcode.cn/problems/fibonacci-number/ 文章链接&#xff1a;https://programmercarl.com/0509.斐波那契数.html 视频链接&#xff1a;https://www.bilibili.com/video/BV1f5411K7mo 思路&#xff1a; 动态规划步骤…

django-ckeditor富文本编辑器

一.安装django-ckeditor 1.安装 pip install django-ckeditor2.注册应用 INSTALLED_APPS [...ckeditor&#xff0c; ]3.配置model from ckeditor.fields import RichTextFieldcontent RichTextField()4.在项目中manage.py文件下重新执行迁移&#xff0c;生成迁移文件 py…

jvm优化

1.jvm组成 什么是jvm&#xff0c;java是跨平台语言&#xff0c;对不同的平台&#xff08;windos&#xff0c;linux&#xff09;&#xff0c;有不同的jvm版本。jvm屏蔽了平台的不同&#xff0c;提供了统一的运行环境&#xff0c;让java代码无需考虑平台的差异。 jdk包含jre包含…

ValueError和KeyError: ‘bluegrass’的问题解决

项目场景&#xff1a; 项目相关背景&#xff1a; 问题描述 遇到的问题1&#xff1a; KeyError: ‘bluegrass’ 不能识别某标签 遇到的问题2&#xff1a; xml etree.fromstring(xml_str) ValueError: Unicode strings with encoding declaration are not supported. Please …

K8S POD控制器:从基础到高级实战技巧

一、引言 在当今的云计算时代&#xff0c;Kubernetes&#xff08;K8s&#xff09;已成为最受欢迎的容器编排工具&#xff0c;它的核心组成部分之一——K8s POD控制器&#xff0c;扮演着至关重要的角色。这篇文章旨在深入探讨K8s POD控制器的内部工作原理、不同类型及其应用场景…

【数据结构】树和二叉树及堆的深入理解

【数据结构】树和二叉树及堆的深入理解 &#x1f525;个人主页&#xff1a;大白的编程日记 &#x1f525;专栏&#xff1a;数据结构 文章目录 【数据结构】树和二叉树及堆的深入理解前言一.树1.1 树的概念1.2 树的相关概念1.3 树的表示1.4 树的应用 二.二叉树2.1 二叉树概念及…

clion中建立c文件工程,读取或创建sqlite3数据库文件

1.首先前往SQLite官网下载sqlite3所需文件 SQLite Download Page 2.解压文件&#xff0c;将其中的sqlite3.c和sqlite3.h拷贝到你对应的文件工程中 3.修改CMakeLists.txt文件&#xff0c;添加编译选项及连接文件 4.运行代码及查询数据库文件

实战:SpringBoot 15个功能强大Bean

下面这15个bean&#xff0c;可以很方便的帮我们获取当前环境信息&#xff0c;运行信息&#xff0c;参数信息等等 1. 应用程序参数Environment和ApplicationArguments SpringBoot程序在启动时&#xff0c;可以通过如下方式设置启动参数&#xff1a; java -jar app.jar --pack…

单链表算法 - 链表分割

链表分割_牛客题霸_牛客网现有一链表的头指针 ListNode* pHead&#xff0c;给一定值x&#xff0c;编写一段代码将所有小于x的。题目来自【牛客题霸】https://www.nowcoder.com/practice/0e27e0b064de4eacac178676ef9c9d70思路: 代码: /* struct ListNode {int val;struct List…

iterator(迭代器模式)

引入 在想显示数组当中所有元素时&#xff0c;我们往往会使用下面的for循环语句来遍历数组 #include <iostream> #include <vector>int main() {std::vector<int> v({ 1, 2, 3 });for (int i 0; i < v.size(); i){std::cout << v[i] << &q…

正则表达式(Ⅲ)——分组匹配

简介 为了给表达式分组&#xff0c;我们需要将文本包裹在 () 中 有点类似于匹配子串&#xff0c;只不过是找出所有的子串&#xff0c;并且拼成一组 分组之间需要有分割符&#xff0c;,或者-或者_都可以 直接分组 引用分组 这个比较难以理解 \1和\2的作用有两个&#xff1a…

Ubuntu系统修改SSH默认端口号

1.查看系统和系统版本号 rootecs-c0fe:~# lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 16.04.7 LTS Release: 16.04 Codename: xenial rootecs-c0fe:~# uname -a Linux ecs-c0fe 4.4.0-210-generic #242-Ubunt…

Python入门------多个版本--虚拟环境的创建(非anaconda方式)

说明介绍&#xff1a; 1. 什么是虚拟环境 在Python中&#xff0c;虚拟环境&#xff08;Virtual Environment&#xff09;是一个独立的、隔离的Python运行环境&#xff0c;它拥有自己的Python解释器、第三方库和应用程序。通过创建虚拟环境&#xff0c;可以确保项目之间的依赖关…

Python 实验五 高级数据结构

一、实验目的 &#xff08;1&#xff09;掌握序列的基本操作 &#xff08;2&#xff09;掌握集合、字典的基本操作 二、实验环境 联网计算机一台/每人&#xff0c;内装Windows 7以上操作系统和安装Python 3.7集成开发环境IDLE。 三、实验内容 Sy5-1 列表实现。编写一个…

数据库的约束条件和用户管理

约束条件&#xff1a; 主键&#xff1a;主键约束 primary key 用于标识表中的主键列的值&#xff0c;而且这个值是全表当中唯一的&#xff0c;而且只不能为null 一个表只能有一个主键。 外键&#xff1a;用来建立表与表之间的关系。确保外键中的值于另一个表的主键值匹配&a…

camtasia怎么剪掉不用的部分 屏幕录制的视频怎么裁剪上下不要的部分 camtasia studio怎么裁剪视频时长 camtasia怎么剪辑视频教程

有时我们录制的屏幕内容&#xff0c;并不一定全部需要。那么&#xff0c;屏幕录制的视频怎么裁剪上下不要的部分&#xff1f;可以使用视频剪辑软件&#xff0c;或者微课制作工具来进行裁剪。屏幕录制的视频怎么旋转&#xff1f;录制视频的旋转也是一样的&#xff0c;均在编辑步…

可视化工具选择指南:助力企业数字化转型和新质生产力发展

随着信息技术的快速发展和新质生产力概念的兴起&#xff0c;可视化工具在各个行业中的作用日益凸显。这些工具不仅能够帮助用户更直观地理解和分析数据&#xff0c;还能提升团队的协作效率和决策质量。 在当今数字化转型迅速发展的背景下&#xff0c;新质生产力的概念正在成为…

Spring Boot项目的404是如何发生的

问题 在日常开发中&#xff0c;假如我们访问一个Sping容器中并不存在的路径&#xff0c;通常会返回404的报错&#xff0c;具体原因是什么呢&#xff1f; 结论 错误的访问会调用两次DispatcherServlet&#xff1a;第一次调用无法找到对应路径时&#xff0c;会给Response设置一个…

【React笔记初学总结一】React新手的学习流程笔记总结,掰开了揉碎了,下载安装基础结构学习

REACT学习记录 一、React是什么&#xff1a;二、尝试安装下载&#xff1a;三、理解都有什么四、基础网页学习&#xff1a;1.几个比较重要的资源包例子2.第一个react示例&#xff1a;&#xff08;掰开了揉碎了&#xff0c;咱们先看懂它最简单的结构&#xff09;3.第二个react示例…