• Java创建多线程的四种方式


    爱人如己。

    多线程

            多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。

            多线程是并行化的一种形式,或者是拆分工作以便同时进行处理。线程化的程序将工作拆分到多个软件线程,而不是将大量工作交给单个内核。这些线程由不同的 CPU 内核并行处理,以节省时间。

            进程是程序的一次执行,进程至少包含一个线程,也就是主线程main_thread,同一个进程内的所有线程共享这个进程的所有资源。

            资源包括:线程可以操作进程内存空间中的全局变量、静态变量等。 线程可以共享进程打开的文件描述符,这意味着多个线程可以同时对同一个文件进行读写操作,不同线程之间的文件操作是相互可见的。

            正是由于多个线程可以并行(具有多核cpu)共享进程的资源,所以会导致共享数据的一致性等问题,所以要使用互斥锁、信号量、条件变量等同步机制保证共享数据的一致性。

    线程的生命周期

    新建、就绪、运行、阻塞、死亡

    Java使用多线程的方式

    1、继承Thread类

    1. package com.lin.java;
    2. /**
    3. * 线程的创建方式一:继承Thread类
    4. * 1.创建继承于Thread类的子类
    5. * 2.重写Thread类中的run()方法,用于描述线程要执行的操作
    6. * 3.创建Thread类的子类的对象
    7. * 4.通过对象调用start()方法
    8. *
    9. * @author shkstart
    10. * @create 2021-08-12 10:47
    11. */
    12. public class ThreadTest1 {
    13. public static void main(String[] args) {// 主线程
    14. // 3.创建Thread类的子类的对象
    15. MyThread t1 = new MyThread();
    16. t1.setName("线程1");// 给线程起名字
    17. // 4.通过对象调用start()方法
    18. // start()方法的作用:①启动当前线程 ②并执行当前线程对象的run()方法
    19. // 注意:一个线程对象只能调用一次start()方法
    20. t1.start();// 主线程执行到这,开始执行另一个线程
    21. // 在创建一个进程:输出100以内的偶数
    22. MyThread t2 = new MyThread();
    23. t2.setName("线程2");
    24. t2.start();// 此时相当于三个线程同时执行
    25. Thread.currentThread().setName("主线程");
    26. for (int i = 0;i < 100;i++){// 主线程和另外两个线程t1 t2同时的执行
    27. if (i % 2 == 0){
    28. System.out.println(Thread.currentThread().getName() + " " + i);
    29. }
    30. }
    31. }
    32. }
    33. /*
    34. 线程的创建方式一:继承Thread类
    35. 创建一个线程用来输出100以内的偶数
    36. */
    37. // 1.创建继承于Thread类的子类,说明此类是线程类
    38. class MyThread extends Thread{
    39. // 2.重写Thread类中的run()方法,用于描述线程要执行的操作
    40. @Override
    41. public void run() {
    42. // 线程要执行的操作:用来输出100以内的偶数
    43. for (int i = 0;i < 100;i++){
    44. if (i % 2 == 0){
    45. System.out.println(Thread.currentThread().getName() + " " + i);
    46. }
    47. }
    48. }
    49. }

    2、实现Runnable接口

    1. package com.lin.java;
    2. /**
    3. * 创建线程的方式二:实现Runnable接口
    4. * 1.创建Runnable接口的实现类
    5. * 2.实现Runnable接口中的run()方法,将线程要执行的操作写在此方法中
    6. * 3.创建Runnable接口的实现类的对象
    7. * 4.将Runnable接口的实现类的对象作为参数传递给Thread的构造器,创建Thread对象
    8. * 5.通过Thread类的对象调用start()方法
    9. *
    10. *
    11. * 比较创建线程的两种方式。
    12. * * 开发中:优先选择:实现Runnable接口的方式
    13. * * 原因:1. 实现的方式没有类的单继承性的局限性
    14. * * 2. 实现的方式更适合来处理多个线程有共享数据的情况。(原因见WindowTest1.java)
    15. * *
    16. * * 联系:public class Thread implements Runnable
    17. * * 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
    18. * *
    19. *
    20. * @author shkstart
    21. * @create 2021-08-15 11:23
    22. */
    23. public class ThreadTest2 {
    24. public static void main(String[] args) {
    25. // 3.创建Runnable接口的实现类的对象
    26. NumThread2 numThread2 = new NumThread2();
    27. // 4.将Runnable接口的实现类的对象作为参数传递给Thread的构造器,创建Thread对象
    28. // 此步其实是创建一个线程,但构造器需要Runnable接口对象
    29. Thread thread = new Thread(numThread2);
    30. thread.setName("线程1");
    31. // 5.通过Thread类的对象调用start()方法
    32. thread.start();
    33. // 再创建一个线程
    34. Thread thread1 = new Thread(numThread2);
    35. thread1.setName("线程2");
    36. thread1.start();
    37. }
    38. }
    39. // 1.创建Runnable接口的实现类
    40. class NumThread2 implements Runnable{
    41. // 2.实现Runnable接口中的run()方法,将线程要执行的操作写在此方法中
    42. @Override
    43. public void run() {
    44. for (int i = 0; i < 100; i++) {
    45. if (i % 2 == 0){
    46. System.out.println(Thread.currentThread().getName() + ":" + i);
    47. }
    48. }
    49. }
    50. }

    3、实现Callable接口

    1. package com.lin.java;
    2. import java.util.concurrent.Callable;
    3. import java.util.concurrent.ExecutionException;
    4. import java.util.concurrent.FutureTask;
    5. /**
    6. * 创建线程的方式三:实现Callable接口 --- JDK 5.0新增
    7. * 说明:1.需要借助FutureTask类,比如可以获取call()方法中返回值 ---FutureTask类中的get()
    8. * 2.FutureTask类实现了Runnable 和 Callable 接口
    9. * 3.FutureTask类即可以做为Runnable被线程执行,又可以做为Future获取call方法中的返回值
    10. *
    11. *
    12. * 1.创建Callable接口的实现类
    13. * 2.实现Callable接口中的call()方法,将线程要执行的操作写在此方法中
    14. * 3.创建创建Callable接口的实现类的对象
    15. * 4.将Callable接口的实现类的对象作为参数传递给FutureTask的构造器,并创建FutureTask对象
    16. * 5.将FutureTask对象作为参数传递给Thread类的构造器,创建Thread对象,并调用start()方法
    17. *
    18. *
    19. *
    20. * 如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
    21. * 优点:1.线程有返回值 call()
    22. * 2.可以抛出异常,在外部调用可以捕获异常 call()
    23. * 3.Callable是支持泛型的
    24. *
    25. * @author shkstart
    26. * @create 2021-08-15 9:16
    27. */
    28. public class ThreadTest3 {
    29. public static void main(String[] args) {
    30. Thread.currentThread().setName("主线程");
    31. // 3.创建创建Callable接口的实现类的对象
    32. MyTread3 t = new MyTread3();
    33. // 4.将Callable接口的实现类的对象作为参数传递给FutureTask的构造器,并创建FutureTask对象
    34. FutureTask futureTask = new FutureTask(t);// 创建FutureTask对象需要Callable接口对象作为构造器参数
    35. // 5.将FutureTask对象作为参数传递给Thread类的构造器,创建Thread对象,并调用start()方法
    36. Thread thread = new Thread(futureTask);// 创建线程Thread对象需要Runnable接口对象作为构造器参数,此时FutureTask类的对象做为Runnable被线程执行
    37. thread.setName("分线程");
    38. thread.start();// 启动线程
    39. // 6.获取Callable中call方法的返回值
    40. try {
    41. // futureTask.get()用于获取线程方法call()的返回值
    42. System.out.println("总和为:" + futureTask.get());// 此时FutureTask作为Future获取call方法中的返回值
    43. } catch (InterruptedException e) {
    44. System.out.println(e.getMessage());
    45. } catch (ExecutionException e) {
    46. System.out.println(e.getMessage());
    47. }
    48. // 错误的,仍然是主线的做的,并没有启动线程做
    49. // try {
    50. // t.call();
    51. // } catch (Exception e) {
    52. // e.printStackTrace();
    53. // }
    54. }
    55. }
    56. // 1.创建Callable接口的实现类
    57. class MyTread3 implements Callable{
    58. // 2.实现Callable接口中的call()方法,将线程要执行的操作写在此方法中
    59. @Override
    60. public Object call() throws Exception {
    61. int sum = 0;
    62. int i = 1;
    63. for (; i <= 100; i++) {
    64. System.out.println(Thread.currentThread().getName() + i);
    65. sum += i;
    66. }
    67. // if (i == 101){
    68. // // 手动抛出一个异常对象
    69. // throw new Exception("我是call方法中的异常");
    70. // }
    71. return sum;
    72. }
    73. }

    4、使用线程池

    (1)为什么要使用线程池?

    方便管理和调控,因为线程池中的线程都是提前创建好的,可以节省创建线程的时间。重复利用线程池中的线程避免或者说降低了重复创建线程和的消耗。

    创建和销毁线程的消耗所在

    创建线程:

            当创建一个线程时,操作系统需要为线程分配一定的资源,包括线程栈空间、线程标识符等。
            在 Java 中,创建线程需要分配内存空间来存储线程的执行栈、线程状态等信息。


    销毁线程(一般是在线程的run方法执行完毕或者被中断,线程就会自动销毁掉):

            当线程执行完成或者被中断时,操作系统需要释放线程所占用的资源。
            在 Java 中,线程销毁时会释放线程对象所占用的内存空间,包括线程栈空间等资源。


    尽管线程的创建和销毁过程很快,但是它们仍然需要消耗一定的系统资源。因此,在设计和开发多线程应用程序时,需要合理地管理线程的生命周期,避免不必要的线程创建和销毁,以减少系统资源的消耗。

    线程池工作原理:

            例如线程池中最多可以允许创建三个工作线程, 也叫核心线程, 前面三个任务来的时候会给前面三个任务单独创建三个线程; 但是后面任务再来的时候, 因为创建的工作线程已达到最大数, 那么后面的任务就会进入任务队列中排队等待; 等前面的任务执行完成, 有空闲的线程的时候使用空闲的线程依次执行任务队列中的任务。

    参数名作用
    corePoolSize核心线程数量
    maximumPoolSize最大线程池数量
    keepAliveTime线程池中超过corePoolSize数目的空闲线程最大存活时间;可以allowCoreThreadTimeOut(true)使得核心线程有效时间
    TimeUnitkeepAliveTime时间单位
    workQueue阻塞任务队列
    threadFactory新建线程工厂
    RejectedExecutionHandler当提交任务数超过maxmumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理

    1. // Java线程池类
    2. // 一、ThreadPoolExecutor
    3. public ThreadPoolExecutor(int corePoolSize,
    4. int maximumPoolSize,
    5. long keepAliveTime,
    6. TimeUnit unit,
    7. BlockingQueue workQueue,
    8. ThreadFactory threadFactory,
    9. RejectedExecutionHandler handler) {}
    10. 1、corePoolSize 线程池中的核心线程数
    11. 当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize, 即使有其他空闲线程能够执行新来的任务, 也会继续创建线程;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。核心线程即使执行完任务也不会停止,会继续存在等待新任务。
    12. 设置规则:
    13. (1)CPU密集型(CPU密集型也叫计算密集型,指的是运算较多,cpu占用高,读/写I/O(硬盘/内存)较少):corePoolSize = CPU核数 + 1
    14. (2)IO密集型(与cpu密集型相反,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。):corePoolSize = CPU核数 * 2
    15. 2、workQueue 用来暂时保存任务的工作队列:
    16. (1)ArrayBlockingQueue: 基于数组结构的有界阻塞队列,按FIFO先进先出排序任务;
    17. (2)LinkedBlockingQueue: 基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQueue;
    18. (3)SynchronousQueue: 一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue;
    19. (4)PriorityBlockingQueue: 具有优先级的无界阻塞队列;
    20. LinkedBlockingQueue比ArrayBlockingQueue在插入删除节点性能方面更优,但是二者在put(), take()任务的时均需要加锁,SynchronousQueue使用无锁算法,根据节点的状态判断执行,而不需要用到锁,其核心是Transfer.transfer().
    21. 3、maximumPoolSize 线程池中允许的最大线程数,maximumPoolSize - corePoolSize就是非核心线程
    22. 最大线程数,默认为Integer.MAX_VALUE,一般设置为和核心线程数一样
    23. 4、keepAliveTime 空闲线程的存活时间,非核心线程空闲超过这个时间则销毁
    24. 5、unit keepAliveTime的单位
    25. 6、threadFactory 创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名。默认为DefaultThreadFactory
    26. 7、handler 拒绝策略,当核心线程数满了,工作队列也满了,线程数达到了线程池允许的最大线程数时,继续提交任务的策略:
    27. AbortPolicy: 直接抛出异常,默认策略;
    28. CallerRunsPolicy: 用调用者所在的线程来执行任务;
    29. DiscardOldestPolicy: 丢弃工作队列中靠最前的任务,并执行当前任务
    30. DiscardPolicy: 直接丢弃任务;
    31. // 任务执行
    32. ThreadPoolExecutor有两种提交任务执行的方式:ThreadPoolExecutor.submit()有返回值方便异常处理 和 execute()没有返回值
    33. // submit和execute提交任务执行后:
    34. 1、当线程池中的线程数小于corePoolSize时,新提交任务将创建一个新线程去执行任务,即使此时线程池中存在空闲线程。
    35. 2、当线程池中的线程数达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
    36. 3、如果workQueue已满且线程数
    37. 4、如果workQueue已满且线程数=maximumPoolSize,则执行拒绝策略
    38. // 二、ThreadPoolTaskExecutor
    39. /**
    40. * 1、ThreadPoolTaskExecutor
    41. * 对ThreadPoolExecutor的进一步封装,实际应用中一般使用ThreadPoolTaskExecutor而不是 ThreadPoolExecutor。
    42. * 使用ThreadPoolExecutor创建线程池需要指定很多参数,ThreadPoolTaskExecutor只有一个无参构造函数,后续可以通过set设置更灵活。
    43. *
    44. * 使用ThreadPoolTaskExecutor无需设置阻塞队列类型,只需要按需设置队列大小即可。ThreadPoolTaskExecutor底层使用的队列为:
    45. * 队列容量大于0则使用 LinkedBlockingQueue队列;否则使用 SynchronousQueue队列;
    46. *
    47. * 2、使用@Bean方式注入,保证线程池对象只创建一次
    48. * @return
    49. */
    50. @Bean(name = "threadPoolTaskExecutor")
    51. public ThreadPoolTaskExecutor threadPoolTaskExecutor()
    52. {
    53. ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    54. executor.setMaxPoolSize(maxPoolSize);
    55. executor.setCorePoolSize(corePoolSize);
    56. executor.setQueueCapacity(queueCapacity);
    57. executor.setKeepAliveSeconds(keepAliveSeconds);
    58. // 线程池对拒绝任务(无线程可用)的处理策略
    59. executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    60. return executor;
    61. }
    62. // 三、ScheduledThreadPoolExecutor
    63. /**
    64. * 执行周期性或定时任务
    65. *
    66. * ScheduledThreadPoolExecutor
    67. * 1、ScheduledThreadPoolExecutor继承自ThreadPoolExecutor。它主要用于在给定的延迟之后运行任务,或者定期执行任务。
    68. * 像其他线程池,提交了任务就会立马执行。
    69. * ScheduledThreadPoolExecutor的功能与Timer类似,但 ScheduledThreadPoolExecutor功能更强大、更灵活。
    70. * Timer对应的是单个后台线程,而 ScheduledThreadPoolExecutor可以在构造函数中指定多个对应的后台线程数。
    71. */
    72. https://blog.csdn.net/qq_43410878/article/details/123732981
    1. package com.lin.java;
    2. import java.util.concurrent.*;
    3. /**
    4. * 创建线程的方式四:使用线程池二
    5. * 直接创建ThreadPoolExecutor对象,并指定参数,推荐使用
    6. *
    7. * execute()或submit()方法后:
    8. * 优先使用核心线程池中的线程去执行任务。当阻塞任务队列满了且核心线程池中没有空闲线程才使用非核心线程池中的线程。
    9. * 1.当线程池中的线程数小于corePoolSize时,新提交任务将创建一个新线程去执行任务,即使此时线程池中存在空闲线程。
    10. * 2.当线程池中的线程数达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
    11. * 3.当workQueue已满,且maximumPoolSize>corePoolSize时,则在非核心线程池中创建新线程执行新任务
    12. * 4.当提交任务数超过maximumPoolSize时,就是创建的线程数且不是空闲(在执行任务)超过了最多线程数,新提交任务由RejectedExecutionHandler处理
    13. *
    14. * 5.当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
    15. * 6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
    16. *
    17. * 这个线程创建了,执行完任务后,也不会立即关闭,而是等待cpu调用继续执行任务,所以可以节省创建和销毁线程的时间。
    18. * 当任务到达时,任务可以不需要等到线程创建就能立即执行。
    19. *
    20. * @author shkstart
    21. * @create 2022-05-21 17:01
    22. */
    23. public class ThreadTest5 {
    24. public static void main(String[] args) {
    25. // 使用给定的参数和默认的线程工厂以及默认的拒绝策略创建一个线程池
    26. ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5));
    27. threadPoolExecutor.execute(new NumThreadNew());// 提交任务给线程池处理,这个是处理没有返回值的任务
    28. // 有返回值
    29. Future result = threadPoolExecutor.submit(new NumSumNew());// 和execute区别就是提交的任务是有返回值且可以捕获异常的
    30. try {
    31. Object o = result.get();
    32. System.out.println("线程执行的返回值" + o);
    33. } catch (InterruptedException e) {
    34. e.printStackTrace();
    35. } catch (ExecutionException e) {
    36. e.printStackTrace();
    37. }
    38. System.out.println("线程池中的线程数量=" + threadPoolExecutor.getPoolSize());
    39. }
    40. }
    41. // 输出100内的偶数的线程
    42. class NumThreadNew implements Runnable{
    43. @Override
    44. public void run() {
    45. for (int i = 0; i < 100; i++) {
    46. if (i % 2 == 0){
    47. System.out.println(Thread.currentThread().getName() + ":" + i);
    48. }
    49. }
    50. }
    51. }
    52. // 输出100内的奇数的线程
    53. class NumThread1New implements Runnable{
    54. @Override
    55. public void run() {
    56. for (int i = 0; i < 100; i++) {
    57. if (i % 2 == 0){
    58. System.out.println(Thread.currentThread().getName() + ":" + i);
    59. }
    60. }
    61. }
    62. }
    63. // 输出10以内数字以及计算10以内数字和的线程
    64. class NumSumNew implements Callable {
    65. @Override
    66. public Object call() throws Exception {
    67. int sum = 0;
    68. for (int i = 0; i < 10; i++) {
    69. System.out.println(Thread.currentThread().getName() + ":" + i);
    70. sum += i;
    71. }
    72. return sum;
    73. }
    74. }

    1. package com.lin.java;
    2. import java.util.concurrent.Callable;
    3. import java.util.concurrent.ExecutorService;
    4. import java.util.concurrent.Executors;
    5. import java.util.concurrent.ThreadPoolExecutor;
    6. /**
    7. * 创建线程的方式四:使用线程池一
    8. * 使用Executors工具类,这种不推荐用,具体原因百度。
    9. *
    10. * 好处:
    11. * * 1.提高响应速度(减少了创建新线程的时间)
    12. * * 2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    13. * * 3.便于线程管理
    14. * * corePoolSize:核心线程数
    15. * * maximumPoolSize:最大线程数
    16. * * keepAliveTime:线程没有任务时最多保持多长时间后会终止
    17. * *
    18. * *
    19. * * 面试题:创建多线程有几种方式?四种!
    20. *
    21. * @author shkstart
    22. * @create 2021-08-15 10:51
    23. */
    24. public class ThreadTest4 {
    25. public static void main(String[] args) {
    26. //1. 提供指定线程数量的线程池--service、service1
    27. ExecutorService service = Executors.newFixedThreadPool(10);// 这个返回值是ThreadPoolExecutor对象
    28. ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
    29. // 设置线程池的属性
    30. // service1.setCorePoolSize(15);
    31. //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
    32. // 参数是指定要完成什么操作的线程对象
    33. service1.execute(new NumThread());// 执行任务,适用于实现Runnable,无返回值
    34. service1.execute(new NumThread1());
    35. // service1.submit();// 适合使用于Callable,有返回值
    36. System.out.println("线程池中线程的数量" + service1.getPoolSize());
    37. // 3.关闭连接池
    38. service1.shutdown();
    39. }
    40. }
    41. // 输出100内的偶数的线程
    42. class NumThread implements Runnable{
    43. @Override
    44. public void run() {
    45. for (int i = 0; i < 100; i++) {
    46. if (i % 2 == 0){
    47. System.out.println(Thread.currentThread().getName() + ":" + i);
    48. }
    49. }
    50. }
    51. }
    52. // 输出100内的奇数的线程
    53. class NumThread1 implements Runnable{
    54. @Override
    55. public void run() {
    56. for (int i = 0; i < 100; i++) {
    57. if (i % 2 == 0){
    58. System.out.println(Thread.currentThread().getName() + ":" + i);
    59. }
    60. }
    61. }
    62. }
    63. // 输出10以内数字以及计算10以内数字和的线程
    64. class NumSum implements Callable{
    65. @Override
    66. public Object call() throws Exception {
    67. int sum = 0;
    68. for (int i = 0; i < 10; i++) {
    69. System.out.println(Thread.currentThread().getName() + ":" + i);
    70. sum += i;
    71. }
    72. return sum;
    73. }
    74. }

    CompletableFuture-异步任务编排

    上面所使用的线程技术,如果想要控制两个异步任务之间的先后性,是比较麻烦的。

    而CompletableFuture就是帮你处理这些任务之间的逻辑关系,CompletableFuture提供了很多编排任务的API,编排好任务的执行方式后,任务会按照规划好的方式一步一步执行,不需要让业务线程去频繁的等待。

    总之,CompletableFuture类使得并发任务的处理变得简单而高效,通过简洁的API,开发者能轻松创建、组合和链式调用异步操作,无需关心底层线程管理,这不仅提升了程序的响应速度,还优化了资源利用率,让复杂的并发逻辑变得易于掌控。

    参考连接:

    CompletableFuture使用详解(超详细)-CSDN博客

    第25课:CompletableFuture - 知乎 (zhihu.com)

    Java并发基础:CompletableFuture全面解析 (baidu.com)

  • 相关阅读:
    WOT全球技术创新大会2022:腾讯云数据库专场 + 云原生解决方案专场
    自定义事件
    Undefined和Null的区别
    Pytorch 下 TensorboardX 使用
    React之路由
    用Blender制作YOLO目标检测器训练数据
    VMware虚拟机+Centos7 配置静态,动态IP
    【超好懂的比赛题解】第 45 届国际大学生程序设计竞赛(ICPC)亚洲区域赛(济南)
    Mysql批量插入数据时如何解决重复问题
    基于Echarts实现可视化数据大屏机械设备监测大数据统计平台HTML页面
  • 原文地址:https://blog.csdn.net/weixin_52938172/article/details/126957977