Java多线程——创建线程的几种方式

news2024/11/19 2:33:31

在这里插入图片描述

目录

  • 引出
  • 创建线程有几种方式?
    • 方式1:继承Thread创建线程
    • 方式2:通过Runnable
    • 方式3:通过Callable创建线程
    • 方式4:通过线程池
      • 概述
      • ThreadPoolExecutor API
      • 代码实现
      • 源码分析
      • 工作原理:
        • 线程池的阻塞队列选择
        • 线程池已满又有新任务?
        • 拒绝策略
      • 如何优化线程池配置?
      • Executors
  • 总结

引出

Java多线程——创建线程的几种方式


创建线程有几种方式?

方式1:继承Thread创建线程

public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();
        MyThread t2 = new MyThread();
        t2.start();
    }
}

方式2:通过Runnable

public class App2 {
    public static void main(String[] args) {
        new Thread(()->{
            for (int i = 0; i < 20; i++) {
                System.out.println("i = " + i);
            }
        }).start();
    }
}

方式3:通过Callable创建线程

一个可取消的异步计算。FutureTask提供了对Future的基本实现,可以调用方法去开始和取消一个计算,可以查询计算是否完成并且获取计算结果。只有当计算完成时才能获取到计算结果,一旦计算完成,计算将不能被重启或者被取消,除非调用runAndReset方法。

总的来说,如果你需要在线程任务执行完毕后获取返回结果,或者需要在任务中处理受检查异常,那么你应该使用 Callable 接口。如果你只需要执行一个简单的线程任务而不关心返回结果,那么使用 Runnable 接口更加合适。

package cn.test;
import java.util.concurrent.*;
public class App3 {
    public static void main(String[] args)  {
        //1、计算任务,实现Callable接口
        Callable<String> callable = ()->{
            int sum = 0;
            for (int i = 0; i < 20; i++) {
                sum += i;
                // 耗时操作
                Thread.sleep(100);
            }
            return "计算结果:" + sum;
        };
        //2、创建FutureTask,传入callable对象
        FutureTask<String> futureTask = new FutureTask<>(callable);
        //3、创建启动线程
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
            String result = futureTask.get(1, TimeUnit.SECONDS);
            System.out.println("result = " + result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
            // 超时中断执行
            futureTask.cancel(true);
            System.out.println("超时中断执行");
        }
    }
}

方式4:通过线程池

概述

线程过多会带来额外的开销,频繁创建和销毁大量线程需要占用系统资源,消耗大量时间。其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。

ThreadPoolExecutor API

  public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {

corePoolSize :核心池的大小,如果调用了prestartAllCoreThreads()或者prestartCoreThread()方法,会直接预先创建corePoolSize指定大小的线程,否则当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;这样做的好处是,如果任务量很小,那么甚至就不需要缓存任务,corePoolSize的线程就可以应对;

maximumPoolSize:线程池最大线程数,表示在线程池中最多能创建多少个线程,如果运行中的线程超过了这个数字,那么相当于线程池已满,新来的任务会使用RejectedExecutionHandler 进行处理;

keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止,然后线程池的数目维持在corePoolSize 大小;

unit:参数keepAliveTime的时间单位;

workQueue:一个阻塞队列,用来存储等待执行的任务,如果当前对线程的需求超过了corePoolSize大小,才会放在这里;

threadFactory:线程工厂,主要用来创建线程,比如可以指定线程的名字;

handler:如果线程池已满,新的任务的处理方式

代码实现

public class App4 {
    // 线程池的核心线程数
    private static final int CORE_POOL_SIZE = 5;
    // 线程池的最大线程数
    private static final int MAX_POOL_SIZE = 10;
    // 当线程数大于核心线程数时,多余的空闲线程存活的最长时间
    private static final int KEEP_ALLOW_TIME = 100;
    // 任务队列大小,用来存储等待执行任务的队列
    private static final int QUEUE_CAPACITY = 100;
    public static void main(String[] args) {
        // handler 指定拒绝策略,当提交的任务过多不能及时处理,我们通过定制的策略处理任务
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALLOW_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
        //executor.prestartAllCoreThreads();
        for (int i = 0; i < 10; i++) {
            Runnable runnable = () -> {
                System.out.println(Thread.currentThread().getName() + ":start");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":end");
            };
            // 运行线程
            executor.execute(runnable);
        }
        // 终止线程池
        executor.shutdown();
        while (!executor.isTerminated()) {}
        System.out.println("Finish All");
    }
}

源码分析

/*The main pool control state, ctl, is an atomic integer packing
     * two conceptual fields
     *   workerCount, indicating the effective number of threads
     *   runState,    indicating whether running, shutting down etc   
  */
// 存放线程池的线程池内有效线程的数量 (workerCount)和运行状态 (runState) 
   private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static int workerCountOf(int c) {
        return c & CAPACITY;
    }
    private final BlockingQueue<Runnable> workQueue;
    public void execute(Runnable command) {
        // 如果任务为null,则抛出异常。
        if (command == null)
            throw new NullPointerException();
        // ctl 中保存的线程池当前的一些状态信息
        int c = ctl.get();
        //  下面会涉及到 3 步 操作
        // 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
        // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
        // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
            if (!isRunning(recheck) && remove(command))
                reject(command);
                // 如果当前线程池为空就新创建一个线程并执行。
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
        //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
        else if (!addWorker(command, false))
            reject(command);
    }

工作原理:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
    • 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
    • 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。

3,当一个线程完成任务时,它会从队列中取下一个任务来执行。
4,当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

这样的过程说明,并不是先加入任务就一定会先执行。假设队列大小为 10,corePoolSize 为 3,maximumPoolSize 为 6,那么当加入 20 个任务时,执行的顺序就是这样的:首先执行任务 1、2、3,然后任务 4~13 被放入队列。这时候队列满了,任务 14、15、16 会被马上执行,而任务 17~20 则会抛出异常。最终顺序是:1、2、3、14、15、16、4、5、6、7、8、9、10、11、12、13。

线程池的阻塞队列选择

如果线程数超过了corePoolSize,则开始把线程先放到阻塞队列里,相当于生产者消费者的一个数据通道,有以下一些阻塞队列可供选择:

  1. ArrayBlockingQueue

    ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。

  2. DelayQueue

    DelayQueue阻塞的是其内部元素,DelayQueue中的元素必须实现 java.util.concurrent.Delayed接口,该接口只有一个方法就是long getDelay(TimeUnit unit),返回值就是队列元素被释放前的保持时间,如果返回0或者一个负值,就意味着该元素已经到期需要被释放,此时DelayedQueue会通过其take()方法释放此对象,DelayQueue可应用于定时关闭连接、缓存对象,超时处理等各种场景;

  3. LinkedBlockingQueue

    LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。

  4. PriorityBlockingQueue

    PriorityBlockingQueue是一个没有边界的队列,它的排序规则和 java.util.PriorityQueue一样。需要注意,PriorityBlockingQueue中允许插入null对象。所有插入PriorityBlockingQueue的对象必须实现 java.lang.Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的。

  5. SynchronousQueue

    SynchronousQueue队列内部仅允许容纳一个元素。当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。

使用的最多的应该是LinkedBlockingQueue,注意一般情况下要配置一下队列大小,设置成有界队列,否则JVM内存会被撑爆!

线程池已满又有新任务?

如果线程池已经满了可是还有新的任务提交怎么办?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

线程池已满的定义,是指运行线程数==maximumPoolSize,并且workQueue是有界队列并且已满(如果是无界队列当然永远不会满);

这时候再提交任务怎么办呢?线程池会将任务传递给最后一个参数RejectedExecutionHandler来处理,比如打印报错日志、抛出异常、存储到Mysql/redis用于后续处理等等,线程池默认也提供了几种处理方式,详见下一章:

拒绝策略

拒绝策略指的就是线程池已满情况下任务的处理策略,默认有以下几种:

1、ThreadPoolExecutor.AbortPolicy 中,处理程序遭到拒绝将抛出运行时RejectedExecutionException。

    /**
     * A handler for rejected tasks that throws a
     * {@code RejectedExecutionException}.
     */
    public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an {@code AbortPolicy}.
         */
        public AbortPolicy() { }
        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }

2、在 ThreadPoolExecutor.CallerRunsPolicy,交给线程池调用所在的线程进行处理。

   /**
     * A handler for rejected tasks that runs the rejected task
     * directly in the calling thread of the {@code execute} method,
     * unless the executor has been shut down, in which case the task
     * is discarded.
     */
    public static class CallerRunsPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code CallerRunsPolicy}.
         */
        public CallerRunsPolicy() { }
        /**
         * Executes task r in the caller's thread, unless the executor
         * has been shut down, in which case the task is discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }

3、在 ThreadPoolExecutor.DiscardPolicy 中,直接丢弃后来的任务

 /**
     * A handler for rejected tasks that silently discards the
     * rejected task.
     */
    public static class DiscardPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardPolicy}.
         */
        public DiscardPolicy() { }
        /**
         * Does nothing, which has the effect of discarding task r.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }

4、在 ThreadPoolExecutor.DiscardOldestPolicy 丢弃队列里最老的任务,将当前这个任务继续提交给线程池。

 /**
     * A handler for rejected tasks that discards the oldest unhandled
     * request and then retries {@code execute}, unless the executor
     * is shut down, in which case the task is discarded.
     */
    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardOldestPolicy} for the given executor.
         */
        public DiscardOldestPolicy() { }
        /**
         * Obtains and ignores the next task that the executor
         * would otherwise execute, if one is immediately available,
         * and then retries execution of task r, unless the executor
         * is shut down, in which case task r is instead discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

5、当然也可以自己实现处理策略类,继承RejectedExecutionHandler接口即可,该接口只有一个方法:
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);

如何优化线程池配置?

如何合理配置线程池大小,仅供参考。

一般需要根据任务的类型来配置线程池大小:

如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为【(CPU总核数)】 或者 【(CPU总核数+1)】

如果是IO密集型任务,类似 网络I/O、数据库、磁盘I/O 等,参考值可以设置为【(2 * CPU总核数)】

当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,

再观察任务运行情况和系统负载、资源利用率来进行适当调整。

其中NCPU的指的是CPU的核心数,可以使用下面方式来获取;

   public static void main(String[] args) {
        int ncpu = Runtime.getRuntime().availableProcessors();
        System.out.println("cpu核数 = " + ncpu);
    }

Executors

通过Executors类提供四种线程池。创建方法为静态方式创建。

Executors.newFixedThreadPool();

返回线程池对象。创建的是有界线程池,也就是池中的线程个数可以指定最大数量。

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

可见该方法让keepAliveTime为0,即限制了线程数必须小于等于corePoolSize。而多出的线程则会被无界队列所存储,在其中排队。

Executors.newCachedThreadPool();

创建一个可缓存线程池,线程池长度超过处理需要时,可灵活回收空闲线程,若无可回收线程则新建线程。

   public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

该方法中所有线程均由SynchronousQueue管理,且不设置线程数量上限。对于SynchronousQueue,每个插入线程必须等待另一线程的对应移除操作。(即该队列没有容量,仅试图取得元素时元素才存在)因而,该方法实现了,如果有线程空闲,则使用空闲线程进行操作,否则就会创建新线程。

Executors.newScheduledThreadPool();

创建一个定长线程池,相对于FixedThreadPool,它支持周期性执行和延期执行。

1、延迟3秒执行

public static void main(String[] args) {
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
    executorService.schedule(()->{
        System.out.println(Thread.currentThread().getName()+":线程启动");
    },3, TimeUnit.SECONDS);
    executorService.shutdown();
}

2、每三秒隔一秒执行

public static void main(String[] args) {
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
    executorService.scheduleAtFixedRate(()->{
        System.out.println(Thread.currentThread().getName()+":线程启动");
    },1,3, TimeUnit.SECONDS);
}

Executors.newSingleThreadExecutor();

创建一个单线程线程池,只会用唯一的工作线程执行任务,保证所有任务按FIFO,LIFO的优先级执行。

在实现上,其相当于一个线程数为1的FixedThreadPool

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

总结

Java多线程——创建线程的几种方式

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

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

相关文章

leetcode 热题 100_轮转数组

题解一&#xff1a; 新数组存储&#xff1a;另外用一个数组存储移动后的结果&#xff0c;再复制回原数组。 class Solution {public void rotate(int[] nums, int k) {int[] result new int[nums.length];for (int i 0; i < nums.length; i) {result[(i k) % nums.lengt…

VS配置开发与远程调试笔记

先简单写一下&#xff0c;后续详细补充 场景&#xff1a;本地机器开发&#xff0c;虚拟机调试 准备工作&#xff1a; 由于要将生成的文件生成在虚拟机&#xff0c;避免反复拷贝&#xff0c;直接配置虚拟机共享文件夹进行写入&#xff0c;步骤如下&#xff1a; 虚拟机打开网…

作业1-32 B3620 x 进制转 10 进制

题目 思路 分析题目可知&#xff0c;此题可以用到大写字母&#xff0c;也就是从A开始&#xff0c;分别表示11往后的数字。 那么就用一个for循环&#xff0c;将零到九划分为一个等级&#xff0c;将A到Z划分为一个等级。 for(int i0;i<str.length();i){if(str[i]>0&&…

大模型时代下的自动驾驶研发测试工具链-SimCycle

前言&#xff1a; 最近OpenAI公司的新产品Sora的发布&#xff0c;正式掀起了AI在视频创作相关行业的革新浪潮&#xff0c;AI不再仅限于文本、语音和图像&#xff0c;而直接可以完成视频的生成&#xff0c;这是AI发展历程中的又一座重要的里程碑。AI正在不断席卷着过去与我们息…

万字详解,Java实现低配版线程池

文章目录 1.什么是线程池2.线程池的优势3.原理4.代码编写4.1 阻塞队列4.2 ThreadPool线程池4.3 Worker工作线程4.4 代码测试 5. 拒绝策略5.1 抽象Reject接口5.2 BlockingQueue新增tryPut方法5.3 修改ThreadPool的execute方法5.4 ThreadPool线程池构造函数修改5.5 拒绝策略实现1…

python 基础知识点(蓝桥杯python科目个人复习计划60)

今日复习计划&#xff1a;做题 例题1&#xff1a;可构造的序列总数 问题描述&#xff1a; 构造王国一年一度的构造大赛又开始了&#xff0c;这次构造王国的国王将只给出两个数k和n&#xff0c;需要大家回答出能够构造多少个符合以下条件的序列&#xff1a; 序列的长度为n&a…

机器学习笔记 计算机视觉中的测距任务常见技术路线

一、计算机视觉中的测距任务 测距是计算机视觉中的一项关键任务,涉及测量物体和相机之间的距离。这些信息可用于多种应用,包括机器人、自动驾驶汽车和增强现实。测距技术有很多种,包括主动式和被动式,每种技术都有自己的优点和局限性。主动测距技术,例如飞行时间、结构光和…

达梦数据库将DMHR模式下的表(迁移)导出为EXCEL文件

数据库迁移工具&#xff08;Data Transfer Service&#xff09;位于/dm8/tool/dts.其中/dm8是数据库安装目录。 在创建数据库时我们如果勾选了 “创建示例库DMHR(R)”&#xff0c;数据库实例中就带有这个数据库。 这里是用MobaXterm客户端远程控制ip地址为192.168.148.130的虚…

微服务架构 | 多级缓存

INDEX 通用设计概述2 优势3 最佳实践 通用设计概述 通用设计思路如下图 内容分发网络&#xff08;CDN&#xff09; 可以理解为一些服务器的副本&#xff0c;这些副本服务器可以广泛的部署在服务器提供服务的区域内&#xff0c;并存有服务器中的一些数据。 用户访问原始服务器…

如何将github上代码克隆到本地

1、需求 想把github上一篇论文的代码下载到本地。 2、前提 本地电脑已经安装了Git。&#xff08;没有安装需要到官网下载安装&#xff09; 3、解决方法 大功告成

20 卷积层里的填充和步幅【李沐动手学深度学习v2课程笔记】

1. 填充和步幅 在上下左右分别填充一些0 2. 代码实现 2.1 填充 我们创建一个高度和宽度为3的二维卷积层&#xff0c;并在所有侧边填充1个像素。给定高度和宽度为8的输入&#xff0c;则输出的高度和宽度也是8。 import torch from torch import nn# 为了方便起见&#xff0c;…

【前端】-初始前端以及html的学习

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树&#x1f388; &#x1f389;作者宣言&#xff1a;认真写好每一篇博客&#x1f4a4; &#x1f38a;作者gitee:gitee✨ &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法&#x1f384; 如 果 你 …

实用干货:分享4个冷门但非常实用的HTML属性

大家好&#xff0c;我是大澈&#xff01; 本文约1100字&#xff0c;整篇阅读大约需要2分钟。 关注微信公众号&#xff1a;“程序员大澈”&#xff0c;免费加入问答群&#xff0c;一起交流技术难题与未来&#xff01; 现在关注公众号&#xff0c;免费送你 ”前后端入行大礼包…

详解float函数类型转换

函数描述 float([x]) 函数将数字或数字的字符串表示形式转换为与它等效的有符号浮点数。如果参数x是一个字符串&#xff08;十进制表示的数字串&#xff09;&#xff0c;数字前面可以添加符号来表示正数&#xff0c;或负数。符号和数字之间不能出现空格&#xff0c;但是符号前…

AI数据分析软件-BeepBI的诞生结束了传统BI时代,引领了数据分析零门槛的时代

#AI数据分析# 随着人工智能(AI)的日益成熟&#xff0c;数据分析领域正迎来一场革命性的变革。在这场变革中&#xff0c;DeepBI凭借实现了用ai数据分析&#xff0c;与传统BI工具相比&#xff0c;展现出了前所未有的便捷性和易上手特性&#xff0c;真正实现了数据分析的零门槛。…

灵魂指针,教给(二)

欢迎来到白刘的领域 Miracle_86.-CSDN博客 系列专栏 C语言知识 先赞后看&#xff0c;已成习惯 创作不易&#xff0c;多多支持&#xff01; 目录 一、数组名的理解 二、使用指针访问数组 三、一维数组传参本质 四、冒泡排序 五、二级指针 六、指针数组 七、指针数组…

JavaWeb——014SpringBoot原理(配置优先级、Bean管理、SpringBoot原理)

SpingBoot原理 目录 SpingBoot原理1. 配置优先级2. Bean管理2.1 获取Bean2.2 Bean作用域2.3 第三方Bean 3. SpringBoot原理3.1 起步依赖3.2 自动配置3.2.1 概述3.2.2 常见方案3.2.2.1 概述3.2.2.2 方案一3.2.2.3 方案二 3.2.3 原理分析3.2.3.1 源码跟踪3.2.3.2 Conditional 3.2…

Threejs用切线实现模型沿着轨道行驶

这次讲一个经常遇到的使用场景&#xff0c;让模型沿着轨迹运动&#xff0c;这个场景需要解决两个问题&#xff0c;第一是让模型沿着轨迹运动&#xff0c;第二是在沿着轨迹运动的同时&#xff0c;要保持模型的头部也时刻保持前方&#xff0c;而不是单纯的只是更新模型位置。 还是…

Type-C接口PD协议统一:引领电子科技新纪元的优势解析

在电子科技日新月异的今天&#xff0c;充电接口的统一化已经成为了业界的一大趋势。其中&#xff0c;Type-C接口凭借其传输速度快、使用便捷等优点&#xff0c;迅速成为了市场上的主流选择。而PD&#xff08;Power Delivery&#xff09;协议的统一&#xff0c;更是为Type-C接口…

jenkins+selenium+python实现web自动化测试

jenkinsselenium可以做到对web自动化的持续集成。 Jenkins的基本操作&#xff1a; 一、新建视图及job 新建视图&#xff1a; 新建job&#xff1a; 可以选择构建一个自由风格的软件项目或者复制已有的item 二、准备工作&#xff1a; 安装Jenkins插件&#xff0c;SSH plugin …