探索线程池的威力:优化多线程任务管理与性能提升

比喻举例(可以比作工人队伍)

想象一下,如果我们需要完成很多工作,我们可以招募一群工人来协助。然而,如果每个工人都是临时招募的,工作完成后就解雇他们,那么每次都要花时间和精力来招募和解雇工人,效率会很低。

线程池就像是一个团队,其中包含一些固定数量的常驻工人。这些工人一直在那里,准备接受任务。当有新任务到来时,只需要将任务交给线程池,线程池会选择一个可用的工人来执行任务。

这样做的好处是,线程池中的工人可以被重复利用,无需频繁创建和销毁线程。线程创建和销毁是一个开销较大的操作,通过线程池可以减少这部分开销,提高整体的性能和效率。

线程池还可以控制并发的数量。通过设定线程池的大小,我们可以限制同一时间执行的任务数量,避免资源过度消耗和系统负载过重。线程池可以合理地管理和调度工作,保持系统在高并发情况下的稳定运行。

线程池优点

  1. 降低线程创建和销毁的开销:线程的创建和销毁是一个代价较高的操作。通过使用线程池,我们可以避免频繁地创建和销毁线程,从而减少了这些开销。
  2. 提高系统响应速度:线程池中的线程一直保持活跃状态,可以立即响应新任务的到来。与临时创建的线程相比,线程池中的线程不需要等待线程创建的时间,减少了任务执行的延迟,提高了系统的响应速度。
  3. 控制并发线程数量:线程池可以设定最大并发线程数,以限制同时执行的任务数量。这样可以避免系统过载和资源耗尽,并且可以根据系统的性能和负载情况来灵活调整线程池的大小。
  4. 提供线程管理和调度:线程池通过内部的任务队列来管理待执行的任务。当有新任务提交时,线程池会选择一个可用的线程来执行任务。在任务执行完成后,线程又可以重新回到池中,等待下一个任务的到来。这种自动的管理和调度机制使得线程池更加高效和易于使用。

执行原理

首先讲一下ThreadPoolExecutor中的参数含义

    // Public constructors and methods

    /**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters and default thread factory and rejected execution handler.
     * It may be more convenient to use one of the {@link Executors} factory
     * methods instead of this general purpose constructor.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @throws IllegalArgumentException if one of the following holds:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue} is null
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
名称类型含义🔟
corePoolSizeint线程池中保持活跃的线程数,即使它们是空闲状态,除非设置了 allowCoreThreadTimeOut
maximumPoolSizeint线程池允许存在的最大线程数。
keepAliveTimelong当线程数超过 corePoolSize 时,多余的空闲线程在终止之前等待新任务的最大时间
unitTimeUnit参数的时间单位。
workQueueBlockingQueue任务队列,用于存储尚未执行的任务,这个队列只会存储通过 execute 方法提交的 Runnable 任务。
defaultThreadFactoryThreadFactory线程工厂,用于创建新线程
handlerRejectedExecutionHandler拒绝策略,用于在任务被拒绝执行时的处理方式。

看图解释

图片来自 一个会写诗的程序员

img

在了解了这个的前提下我们就可以看下四种类型

线程池类型

固定大小线程池(FixedThreadPool)

固定大小线程池适用于需要固定数量线程的场景。我们可以通过调用Executors.newFixedThreadPool(int nThreads)来创建固定大小线程池,其中nThreads是线程池中的线程数量。

固定大小线程池的核心线程数和最大线程数都是相同的,因此线程数量是固定的。如果所有的工作线程都忙于执行任务,新的任务就会被放入任务队列中等待执行。由于线程数量固定,所以固定大小线程池不会创建新的线程,直到任务队列满了。

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CustomFixedThreadPoolExample {
    public static void main(String[] args) {
        // 创建固定大小的线程池
        int corePoolSize = 5; // 核心线程数
        int maxPoolSize = 5; // 最大线程数
        long keepAliveTime = 0; // 空闲线程的存活时间(单位:秒)

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>()
        );

        // 提交任务给线程池执行
        executor.execute(new MyTask());

        // 关闭线程池
        executor.shutdown();
    }

    static class MyTask implements Runnable {
        @Override
        public void run() {
            System.out.println("任务正在执行");
        }
    }
}

缓存线程池(CachedThreadPool)

缓存线程池适用于大量短时间任务的场景。我们可以通过调用Executors.newCachedThreadPool()来创建缓存线程池。

缓存线程池的核心线程数为0,最大线程数为整数最大值。当有任务到来时,如果有空闲线程可用,则直接使用。如果没有空闲线程可用,就会创建新的线程。新线程将在60秒钟不活动后被回收。

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CustomThreadPoolExample {
    public static void main(String[] args) {
        // 创建线程池
        int corePoolSize = 0; // 核心线程数
        int maxPoolSize = Integer.MAX_VALUE; // 最大线程数
        long keepAliveTime = 60; // 空闲线程的存活时间(单位:秒)

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            corePoolSize,
            maxPoolSize,
            keepAliveTime,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>()
        );

        // 创建多个任务
        Runnable task1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1正在执行");
            }
        };

        Runnable task2 = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2正在执行");
            }
        };

        Runnable task3 = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务3正在执行");
            }
        };

        // 提交任务到线程池
        executor.execute(task1);
        executor.execute(task2);
        executor.execute(task3);

        // 关闭线程池
        executor.shutdown();
    }
}

//如果想要实现有序的情况下 可以考虑使用executor.submit的方式
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class CustomThreadPoolExample {
    public static void main(String[] args) {
        // 创建线程池
        int corePoolSize = 2; // 核心线程数
        int maxPoolSize = 5; // 最大线程数
        long keepAliveTime = 60; // 空闲线程的存活时间(单位:秒)

        ExecutorService executor = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>()
        );

        // 提交任务
        executor.execute(() -> {
            System.out.println("任务1正在执行");
        });

        executor.execute(() -> {
            System.out.println("任务2正在执行");
        });

        // 关闭线程池
        executor.shutdown();
    }
}

单线程池(SingleThreadExecutor)

单线程池适用于需要按照顺序执行任务的场景。我们可以通过调用Executors.newSingleThreadExecutor()来创建单线程池。

单线程池只有一个工作线程,它会按照任务的提交顺序依次执行任务。如果当前工作线程因为异常退出,线程池会创建一个新的线程来替代它。

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CustomSingleThreadExecutorExample {
    public static void main(String[] args) {
        // 创建单线程池
        int corePoolSize = 1; // 核心线程数
        int maxPoolSize = 1; // 最大线程数
        long keepAliveTime = 0; // 空闲线程的存活时间(单位:秒)

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>()
        );

        // 提交任务给线程池执行
        executor.execute(new MyTask());

        // 关闭线程池
        executor.shutdown();
    }

    static class MyTask implements Runnable {
        @Override
        public void run() {
            System.out.println("任务正在执行");
        }
    }
}

定时线程池(ScheduledThreadPool)

定时线程池用于延迟执行或定时执行任务的场景。我们可以通过调用Executors.newScheduledThreadPool(int corePoolSize)来创建定时线程池。

定时线程池可以按照给定的延迟时间或固定的时间间隔周期执行任务。它使用了内部的ScheduledExecutorService来执行定时任务。

//对于延迟执行任务的实例
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ScheduledTaskExample {
    public static void main(String[] args) {
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);

        // 创建任务1
        Runnable task1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1正在执行");
            }
        };

        // 创建任务2
        Runnable task2 = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2正在执行");
            }
        };

        // 安排任务1在延迟3秒后执行
        long delay1 = 3;
        executor.schedule(task1, delay1, TimeUnit.SECONDS);

        // 安排任务2在延迟5秒后执行
        long delay2 = 5;
        executor.schedule(task2, delay2, TimeUnit.SECONDS);

        // 关闭线程池
        executor.shutdown();
    }
}

//固定时间间隔
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ScheduledTaskExample {
    public static void main(String[] args) {
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);

        // 创建任务
        Runnable task = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务正在执行");
            }
        };

        // 安排任务每隔1秒执行一次
        long initialDelay = 0;
        long period = 1;
        executor.scheduleAtFixedRate(task, initialDelay, period, TimeUnit.SECONDS);

        // 关闭线程池
        executor.shutdown();
    }
}

注意点

为什么不用Executors的方式去建立线程池

  1. 默认参数可能不适合特定的需求:Executors 提供的工厂方法通常使用一组默认的线程池参数,如核心线程数、最大线程数和队列类型等。这些参数对于某些特定的应用场景可能不够合适。如果没有显式地配置这些参数,可能会导致线程池在面对特定负载时表现不佳。
  2. 随意创建线程可能导致资源过度占用:使用 Executors 创建可缓存的线程池时,线程池的最大线程数可以根据需要无限地增长,这意味着可以创建大量的线程。如果不加限制地创建线程,可能会导致系统资源被过度占用,导致性能下降或资源耗尽的风险。
  3. 默认拒绝策略可能不符合特定的需求:当线程池中的线程数达到最大值并且任务队列已满时,Executors 提供了一些默认的拒绝策略,例如抛出异常或丢弃任务。然而,这些默认策略可能不适合特定的应用场景,可能需要自定义拒绝策略来处理满载时的任务。
  4. 隐式创建的线程池可能难以管理和监控:使用 Executors 创建的线程池是隐式的,没有提供直接访问底层线程池实例的方法。这可能导致在需要对线程池进行精确管理、监控和调优时存在困难。

因此,在需要更精确的线程池配置和控制时,手动创建 ThreadPoolExecutor 可以提供更好的灵活性和控制。这样可以更好地满足特定需求,避免不合理的资源占用,并实现更好的性能和可靠性。

优缺点(线程池)

优点缺点
优化资源管理:线程池可以优化线程的创建与销毁,避免频繁创建和销毁线程带来的开销。资源占用:线程池会占用一定的系统资源,包括内存和CPU资源。
提高执行效率:线程池可以重用线程并行执行多个任务,从而提高执行效率。复杂性:线程池的实现较复杂,需要考虑线程池的大小、任务排队策略等。
控制并发数量:线程池可以限制同时执行的线程数量,从而避免资源过度使用和系统负载过高。任务阻塞:线程池在任务队列满时可能会导致新提交的任务等待执行,造成一定的延迟。
统一管理和监控:线程池提供统一的接口来管理和监控线程的执行情况,方便调优和排查问题。适用条件:线程池适用于需要频繁执行、数量较多的任务场景。对于任务较少或执行时间较长的场景,使用线程池可能没有明显的优势。

面试题

什么是线程池?为什么要使用线程池?

  • 线程池是一种用于管理和复用线程的机制。它通过预先创建一组线程,将任务交给这些线程来执行,从而避免了频繁创建和销毁线程的开销。线程池可以提高系统的性能和资源利用率。

Java中常见的线程池有哪些?它们之间有什么区别?

  • Java中常见的线程池有 FixedThreadPoolCachedThreadPoolSingleThreadExecutorScheduledThreadPool
  • FixedThreadPool 在初始化时创建固定数量的线程,适用于长期执行的任务。
  • CachedThreadPool 根据任务的需求动态地调整线程数量,适用于执行大量耗时较短的任务。
  • SingleThreadExecutor 只有一个线程的线程池,适用于需要保证任务按顺序执行的场景。
  • ScheduledThreadPool 可以延迟或定时执行任务的线程池。

线程池中的核心线程数、最大线程数和任务队列有什么作用?

  • 核心线程数是线程池保持的最小线程数量,即使线程是空闲的也会保持存活。
  • 最大线程数是线程池允许的最大线程数量。当线程池已经存在核心线程数,并且任务队列已满时,会创建新的线程直到达到最大线程数。
  • 任务队列用于存储尚未被执行的任务。线程池在执行任务时会从任务队列中取出任务进行处理。

线程池如何处理任务队列已满时的情况?

  • 当任务队列已满且核心线程数达到最大时,线程池会根据指定的拒绝策略来处理新提交的任务。

线程池的拒绝策略有哪些?请分别解释它们的作用。

  • 线程池的拒绝策略有:AbortPolicyCallerRunsPolicyDiscardPolicyDiscardOldestPolicy
  • AbortPolicy 是默认的拒绝策略,它在队列已满的情况下直接抛出 RejectedExecutionException
  • CallerRunsPolicy 将任务交给提交任务的线程去执行,这样可以降低提交任务的速度。
  • DiscardPolicy 直接丢弃无法处理的任务。
  • DiscardOldestPolicy 丢弃队列中最早的未处理任务,尝试重新提交当前任务。

什么是线程池的预启动?如何实现线程池的预启动?

  • 线程池的预启动是指在线程池初始化后,提前创建并启动一定数量的核心线程,使它们处于等待任务的状态。
  • 在 Java 中,通过在创建线程池时使用 prestartCoreThread()prestartAllCoreThreads() 方法来实现线程池的预启动。

线程池中的空闲线程如何管理?它们的存活时间是如何控制的?

  • 线程池中的空闲线程由线程池的控制机制来管理。它们会根据设定的存活时间,在任务执行完毕后保持存活并等待新任务的到来。
  • 存活时间由 keepAliveTime 参数来控制,当空闲线程的存活时间超过设定值时,线程池会销毁这些线程。

在使用线程池时,如何优雅地处理异常?

  • RunnableCallablerun() 方法中,可以使用 try-catch 语句来捕获并处理任务执行过程中的异常。
  • 此外,可以通过实现 ThreadFactory 接口,在创建线程时自定义线程的异常处理机制。

如何监控和调优线程池的性能?

  • 可以使用线程池提供的监控接口,如 getTaskCount()getActiveCount()getCompletedTaskCount() 来查看线程池的运行状态和执行任务的情况。
  • 可以通过调整线程池的参数,如核心线程数、最大线程数和任务队列的大小,以及合理选择拒绝策略来调优线程池的性能。

在什么情况下应该使用自定义线程池,而不是直接使用线程池工厂提供的默认线程池?

  • 使用自定义线程池可以更好地满足特定的业务需求。例如,对于执行长时间任务的场景,可以为线程池设置较大的最大线程数,以提高任务的处理速度。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯上岸每日N题 (闯关)

大家好 我是寸铁 希望这篇题解对你有用&#xff0c;麻烦动动手指点个赞或关注&#xff0c;感谢您的关注 不清楚蓝桥杯考什么的点点下方&#x1f447; 考点秘籍 想背纯享模版的伙伴们点点下方&#x1f447; 蓝桥杯省一你一定不能错过的模板大全(第一期) 蓝桥杯省一你一定不…

使用VisualStudio制作上位机(一)

文章目录 使用VisualStudio制作上位机(一)写在前面第一部分:创建应用程序第二部分:GUI主界面设计使用VisualStudio制作上位机(一) Author:YAL 写在前面 1.达到什么目的呢 本文主要讲怎么通过Visual Studio 制作上位机,全文会以制作过程来介绍怎么做,不会去讲解具体…

【Java】常见面试题:HTTP/HTTPS、Servlet、Cookie、Linux和JVM

文章目录 1. 抓包工具&#xff08;了解&#xff09;2. 【经典面试题】GET和POST的区别&#xff1a;3. URL中不是也有这个服务器主机的IP和端口吗&#xff0c;为啥还要搞个Host&#xff1f;4. 补充5. HTTP响应状态码6. 总结HTTPS工作过程&#xff08;经典面试题&#xff09;7. H…

最长回文子序列——力扣516

动态规划 int longestPalindromeSubseq(string s){int n=s.length();vector<vector<int>>

11. Docker Swarm(二)

1、前言 上一篇中我们利用Docker Swarm搭建了基础的集群环境。那么今天我们就来验证以下该集群的可用性。上一篇的示例中&#xff0c;我创建了3个实例副本&#xff0c;并且通过访问http://192.168.74.132:8080得到我们的页面。 2、验证高可用 1&#xff09;我们可以通过以下命…

一文读懂视频号下载

工具&#xff1a; 移动端抓包工具&#xff08;以Stream为例&#xff09;电脑端浏览器电脑端析包工具&#xff08;以Charles为例&#xff09;【可选项】 一、手机抓包 1 开启Stream 2 抓包 手机进入视频号&#xff0c;通过“搜索“的方式发送get请求&#xff0c;达到抓包的效…

在jupyter notebook中使用海龟绘图

首先&#xff0c;安装ipyturtle3 ref:ipyturtle3 PyPI pip install ipyturtle3然后&#xff0c;安装ipycanvas ipycanvas是一个需要安装在与JupyterLab实例相同环境的包。此外&#xff0c;您需要安装nodejs&#xff0c;并启用JupyterLab ipycanvas小部件。 所有这些都在ipy…

ElasticSearch 数据聚合、自动补全(自定义分词器)、数据同步

文章目录 数据聚合一、聚合的种类二、DSL实现聚合1、Bucket&#xff08;桶&#xff09;聚合2、Metrics&#xff08;度量&#xff09;聚合 三、RestAPI实现聚合 自动补全一、拼音分词器二、自定义分词器三、自动补全查询四、实现搜索款自动补全&#xff08;例酒店信息&#xff0…

C#程序变量统一管理例子 - 开源研究系列文章

今天讲讲关于C#应用程序中使用到的变量的统一管理的代码例子。 我们知道&#xff0c;在C#里使用变量&#xff0c;除了private私有变量外&#xff0c;程序中使用到的公共变量就需要进行统一的存放和管理。这里笔者使用到的公共变量管理库划分为&#xff1a;1)窗体&#xff1b;2)…

Python Django 模型概述与应用

今天来为大家介绍 Django 框架的模型部分&#xff0c;模型是真实数据的简单明确的描述&#xff0c;它包含了储存的数据所必要的字段和行为&#xff0c;Django 遵循 DRY Principle 。它的目标是你只需要定义数据模型&#xff0c;然后其它的杂七杂八代码你都不用关心&#xff0c;…

发布python模仿2023年全国职业的移动应用开发赛项样式开发的开源的新闻api,以及安卓接入案例代码

python模仿2023年全国职业的移动应用开发赛项样式开发的开源的新闻api&#xff0c;以及原生安卓接入案例代码案例 源码地址:keyxh/newsapi: python模仿2023年全国职业的移动应用开发赛项样式开发的开源的新闻api&#xff0c;以及安卓接入案例代码 (github.com) 目录 1.环境配…

服务器感染了.360勒索病毒,如何确保数据文件完整恢复?

引言&#xff1a; 随着科技的不断进步&#xff0c;互联网的普及以及数字化生活的发展&#xff0c;网络安全问题也逐渐成为一个全球性的难题。其中&#xff0c;勒索病毒作为一种危害性极高的恶意软件&#xff0c;在近年来频频袭扰用户。本文91数据恢复将重点介绍 360 勒索病毒&a…

扁线电机定子转子工艺及自动化装备

售&#xff1a;扁线电机 电驱对标样件 需要请联&#xff1a;shbinzer &#xff08;拆车邦&#xff09; 新能源车电机路线大趋势&#xff0c;自动化装配产线需求迫切永磁同步电机是新能源车驱动电机的主要技术路线。目前新能源车上最广泛应用的类型为永磁同步电机&#xff0c…

操作系统-笔记-第二章-进程同步与互斥

目录 二、第二章——【进程同步与互斥】 1、进程同步&#xff08;异步&#xff09; 2、进程互斥 & 共享 3、总结&#xff08;互斥、同步&#xff09; 4、进程互斥&#xff08;软件实现&#xff09; &#xff08;1&#xff09;单标志法——谦让【会让他们轮流访问、其…

【通俗易懂】如何使用GitHub上传文件,如何用git在github上传文件

目录 创建 GitHub 仓库 使用 Git 进行操作 步骤 1&#xff1a;初始化本地仓库 步骤 2&#xff1a;切换默认分支 步骤 3&#xff1a;连接到远程仓库 步骤 4&#xff1a;获取远程更改 步骤 5&#xff1a;添加文件到暂存区 步骤 6&#xff1a;提交更改 步骤 7&#xff1a…

spring如何进行依赖注入,通过set方法把Dao注入到serves

1、选择Generate右键鼠标 你在service层后面方法的这些: 2、UserService配置文件的写法是怎样的&#xff1a; 3、我们在UserController中执行一下具体写法&#xff1a; 最后我们执行一下 &#xff1a; 4、这里可能出现空指针&#xff0c;因为你当前web层,因为你new这个对象根…

spring boot分装通用的查询+分页接口

背景 在用spring bootmybatis plus实现增删改查的时候&#xff0c;总是免不了各种模糊查询和分页的查询。每个数据表设计一个模糊分页&#xff0c;这样代码就造成了冗余&#xff0c;且对自身的技能提升没有帮助。那么有没有办法实现一个通用的增删改查的方法呢&#xff1f;今天…

课程项目设计--项目设计--宿舍管理系统--vue+springboot完成项目--项目从零开始

写在前面&#xff1a; 本文是从项目设计到完成开始写的&#xff0c;本来这个项目基础功能是做完了的&#xff0c;但是之前时间紧张想从头做起了。之前一周写前端后端累死了. 设计是关键&#xff0c;这一篇主讲设计。可能后面会有修改&#xff0c;本人实力有限,学习的也是别人的…

搞定二叉树

树的名词与概念 子树&#xff1a;树是一个有限集合&#xff0c;子树则是该集合的子集。就像套娃一样&#xff0c;一棵树下面还包含着其子树。比如&#xff0c;树T1 的子树为 树T2、T3、T4&#xff0c;树T2的子树为 T5、T6 。 上图中还有许多子树没有标记出来 。 结点(Node)&am…

Stable Diffusion:使用自己的数据集微调训练LoRA模型

Stable Diffusion&#xff1a;使用自己的数据集微调训练LoRA模型 前言前提条件相关介绍微调训练LoRA模型下载kohya_ss项目安装kohya_ss项目运行kohya_ss项目准备数据集生成关键词模型参数设置预训练模型设置文件夹设置训练参数设置 开始训练LoRA模型TensorBoard查看训练情况 测…