Java线程池的设计与使用

news2024/9/22 15:44:42

Java线程池的设计与使用

多线程情景引入

情景分析

  1. 请求积压的情况
    • 系统资源受限: 当大量用户请求同时到来时,服务器受限于内存、CPU、和网络带宽等资源,导致用户长时间等待。
    • 后端处理能力限制: 如频率限制措施(每秒或每几秒的访问限制)在高并发情况下会增加服务器压力,导致请求处理延迟。
    • 线程数限制: 例如,Tomcat服务器在高并发下(如200用户/秒)可能因线程数限制而导致请求积压,延迟数据处理。
  2. 流量突刺情况
    • 第三方服务能力限制: 如使用AI服务每三秒只能处理一个请求,在高并发下突然到来上百个请求,可能会导致AI过载,甚至服务拒绝。
    • AI处理能力限制: AI服务在面对大量同时请求时可能误判为攻击,导致服务限制或拒绝。

解决方法

  • 异步化策略: 识别并处理那些可能导致服务处理能力受限或处理时间过长的场景,如数据量大或第三方服务响应慢的情况。
  • 减少等待时间: 通过异步处理避免用户长时间等待,如在处理繁重任务时采用异步方式。

系统问题分析总结

  • 面对的问题:
    1. 用户等待时间过长: 主要由于AI结果生成的延迟。
    2. 服务器资源紧张: 高并发请求可能导致系统资源紧张,极端情况下可能造成服务器宕机。
    3. 第三方服务能力限制: 如AI服务的处理能力限制(例如每3秒处理1个请求)可能导致处理不及时。
  • 综合对策: 面对上述问题,采用异步化解决方案,优化系统处理能力和用户体验。

异步化

  1. 介绍
    • 同步与异步的对比:
      • 同步: 完成一件事情后才能开始另一件(如烧水后才能开始工作)。
      • 异步: 在处理一件事情的同时,可以进行另一件事情。一旦第一件事完成,系统会通过通知告知,从而进行后续处理(如在烧水的同时处理其他工作,水壶的蜂鸣器通知水烧好,可进行下一步)。
      • 通知机制: 异步化的关键在于知道何时任务已完成,需要一个有效的通知机制。
  2. 异步业务流程分析
    • 在异步流程中,用户提交请求后无需在界面等待,可直接返回主界面或继续其他操作。提交完成后,可以在主页上看到图表生成状态。
    • 消息通知功能: 用于告知用户任务完成情况,比如在界面右上角提供消息通知。
  3. 标准异步化业务流程
    • 流程实施:
      • 用户长时间操作时,提交请求后无需等待,系统先将请求保存至数据库。
      • 将用户任务加入任务队列,由程序或线程按顺序执行。
      • 任务队列类似备忘录,记录待处理事项,按资源可用性处理。
    • 处理策略:
      • 如果任务队列满或线程忙碌,可以选择直接拒绝任务,或记录下来待后续处理。
      • 无论成功与否,应将任务保存到数据库以供后期查阅。
      • 对于提交失败的任务,可在程序空闲时从数据库中提取并执行。
    • 任务状态更新:
      • 程序执行任务后,更新数据库中的任务状态。
      • 为用户提供查询任务状态的功能,以减少无尽等待。
    • 用户体验优化:
      • 异步执行适用于复杂分析,用户可提交新任务或实时查看状态,而非长时间等待。
      • 进度条等可视化工具可用于显示任务进度,提高透明度和用户体验。
  4. 标准异步化流程总结
    • 提交长时间操作请求后,任务保存至数据库,无需用户在界面等待。
    • 根据系统资源状况,任务可能立即执行、进入等待队列,或在失败时被记录待后续处理。
    • 系统从队列中取任务执行,并更新状态。
    • 用户可查询任务状态或接收完成通知(如邮件、系统消息)。

注意点

  • 选择异步化: 并非所有操作都需要异步化。只有在执行时间长的场景中考虑异步化,以避免增加代码复杂度和潜在问题。
  • 异步处理的复杂性: 异步执行中,开发者可能不清楚程序执行到哪一步,因此需要记录每个小任务的状态或进度。
  • 用户体验: 对于复杂任务,提供进度条等可视化工具,以便用户了解任务执行情况,优化体验。

异步化流程与线程池概念

  1. 异步化流程详解

    • 线程角色分配: 设想我们的程序有一个工作者,即线程,例如称之为线程A。此外,存在一个待处理任务的队列。
    • 任务处理: 用户提交任务(如智能分析任务1),任务加入队列。线程小季负责从队列中取出任务执行,但由于线程处理能力有限,一次只能处理一个任务。
    • 多任务情况: 若有第二个任务(任务2)提交,而线程A正在处理任务1,则需考虑调用另一个线程(比如线程B)来处理。如果有更多任务,而可用线程已满,新任务将排队等待处理。
    • 队列溢出处理: 当任务队列满时,为维护系统稳定性,新任务会被记录到数据库中,但暂时不加入队列,待有空闲线程时再加入处理。
    • 任务分配策略: 如何分配任务至线程(比如线程ABCD-Z)需根据实际情况和处理速度来决定。例如,线程A处理速度快,可连续处理多个任务,而线程B处理较慢。、

    线程与任务队列的可视化图解,如下图所示:

    任务加入队列
    线程A可用
    线程B可用
    任务完成
    任务完成
    队列满时
    线程空闲时
    用户提交任务
    任务队列
    线程A处理任务
    线程B处理任务
    返回结果给用户
    记录任务至数据库

    包含以下步骤:

    1. 用户提交任务: 用户的任务请求首先被提交。
    2. 任务加入队列: 提交的任务会被加入到待处理的任务队列中。
    3. 线程处理任务: 根据线程的可用性(例如线程A和线程B),从队列中取出任务进行处理。
    4. 返回结果: 线程完成任务后,结果会返回给用户。
    5. 队列溢出处理: 当任务队列满时,新提交的任务会被记录到数据库中,而不是直接加入队列。
    6. 任务再分配: 当有线程变为空闲状态时,可以从数据库中取出之前记录的任务,重新加入队列处理。
  2. 线程池总结

    • 线程管理的复杂性: 管理线程(如何新增或减少线程)和任务处理(何时接收或拒绝任务)是复杂的。
    • 线程池作用: 线程池协助管理线程,调整任务执行流程,确保高效协调。
    • 线程池的灵活性: 可根据需求设定线程池的最大线程数,线程池会根据任务紧急程度或线程空闲状态来分配任务。
  3. 线程池的实现和应用

    • 实现挑战: 自行实现线程池涉及多方面的考虑,例如何时增加或减少线程,如何防止多个线程抢占同一任务等。
    • 协调与任务窃取: 在Linux环境中,存在一种任务窃取的机制,例如线程A效率高可接手线程B的任务,这需要线程间的有效协调。
    • 数据结构的选择: 实现线程池时,所选用的数据结构(如阻塞队列)对线程管理策略和任务分配具有重要影响。
  4. 注意事项

    • 任务队列的设计: 要考虑任务队列的最大容量和处理策略,以防溢出时影响系统性能。
    • 线程池大小的选择: 依据系统负载和任务特性调整线程池大小,避免资源浪费或处理瓶颈。
    • 任务分配策略: 合理分配任务至线程,考虑任务的紧急程度和线程的处理能力,以提高效率。

线程池的选用与学习

  1. 在Spring框架中实现线程池
    • 使用ThreadPoolTaskExecutor@Async注解: 在Spring框架中,可以通过ThreadPoolTaskExecutor结合@Async注解来实现线程池。这种方法虽然可行,但有一些局限。
    • 局限性: Spring作为一个全面的框架,对线程池进行了封装,这可能隐藏了一些底层细节。对于初学者来说,这种封装可能不利于深入理解线程池的工作原理。
  2. 直接使用Java并发编程包
    • 推荐使用ThreadPoolExecutor: 直接使用Java中的并发编程包,特别是ThreadPoolExecutor,可以实现更灵活和详细的线程池定制。这种方法更适合于需要深入了解和控制线程行为的场景。
    • Java并发编程包的学习建议: 建议在学完Spring Boot并能够实现一个项目,以及在学习完Redis之后,再系统地学习Java并发编程包(Java Concurrency Utilities, JUC)。这样的顺序可以帮助避免过早的学习压力,让学习者在有了一定的实践基础后,更好地理解并发编程的概念和应用。

实践建议

  • 逐步学习: 并发编程是一个复杂且强大的工具,适合在掌握了基础的编程技能和框架使用后进一步学习。
  • 理论与实践相结合: 在学习并发编程的过程中,结合具体的项目实践,可以更加深刻地理解并发控制的细节和挑战。
  • 了解底层实现: 尽管框架如Spring提供了便利的封装,但理解其底层实现(如直接使用Java的并发工具)有助于深入理解并发机制。

Java-JUC并发编程-线程池学习与使用

线程池参数详解

ThreadPoolExecutor 主要有以下几个参数:

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

Java线程池参数的详细讲解:

  1. corePoolSize(核心线程数)
    • 描述:核心线程数类似于公司的正式员工,随时准备处理任务。
    • 作用:保证即使在负载较低时,也有一定数量的线程活跃以应对突发任务。
    • 设定原则:根据允许同时进行的任务数来设定。例如,如果AI服务允许4个任务同时执行,核心线程数应设为4。
  2. maximumPoolSize(最大线程数)
    • 描述:最大线程数定义了线程池在极限情况下的线程数量上限。
    • 作用:确保在高负载情况下,线程池不会超过一定的资源限制。当线程数 = maximumPoolSize, 且任务队列已满,此时添加任务时会触发RejectedExecutionHandler进行处理
    • 设定原则:考虑成本和资源限制。例如,如服务最多允许4个任务同时执行,则最大线程数应设为4。
  3. keepAliveTime(空闲线程存活时间)
    • 描述:这个参数决定了非核心线程(即临时线程)在空闲时会等待多久才被终止。
    • 作用:管理线程池的规模,释放不再需要的线程资源。
    • 设定原则:根据任务频率和资源管理需求来设定,以避免过多占用资源。
  4. TimeUnit(时间单位)
    • 描述:用于设定keepAliveTime参数的时间单位,例如分钟、秒等。
    • 作用:提供灵活性,以便根据具体场景选择合适的时间单位。
  5. workQueue(任务队列)
    • 描述:存储待执行任务的队列,也称为阻塞/工作队列。
    • 作用:缓存待处理任务,确保按顺序执行。
    • 设定原则:需要设定队列长度,因为无限长度的队列可能会消耗过多系统资源。
  6. threadFactory(线程工厂)
    • 描述:负责生成新线程的工厂,类似于公司的人力资源部门。
    • 作用:允许自定义线程的创建,如设定线程名称、优先级等。
  7. RejectedExecutionHandler(拒绝策略)
    • 描述:定义当任务队列满时如何处理新来的任务,例如抛出异常或采用其他策略。
    • 应用:可以设定任务优先级,或者使用资源隔离策略(例如,分别为VIP任务和普通任务设立不同的线程池)。

参数设置参考建议

任务类型考虑

  • 计算密集型任务: 对于CPU密集型任务(如视频处理、图像处理等),corePoolSize建议设置为CPU核数+1,以最大化CPU利用率,减少线程切换。
  • I/O密集型任务: 对于带宽、内存或硬盘读写密集的任务,corePoolSize可以设置得相对较大,一般经验值为2倍CPU核数,但应以实际I/O能力为准。

工作队列(workQueue)的详细描述

在Java线程池中,workQueue是用于存储等待执行的任务的队列。它是线程池的一个重要组成部分,影响着线程池的任务处理策略。以下是几种常见的工作队列类型及其特点:

  1. ArrayBlockingQueue - 有界队列

    • 特点: 这是一个基于数组结构的有界阻塞队列,需要在创建时指定队列的大小。
    • 适用场景: 当希望线程池处理任务的数量有一个明确的上限时,以避免资源耗尽。
    • 有界队列(如ArrayBlockingQueue)可以防止资源耗尽,但可能导致新任务在队列满时被拒绝。
  2. LinkedBlockingQueue - 链表队列

    • 有界与无界:
      • 如果在创建时指定了大小,它表现得与ArrayBlockingQueue相似。
      • 如果未指定大小,它将变成一个实际上的无界队列,其最大容量为Integer.MAX_VALUE
    • 适用场景: 适用于任务处理不需要严格的数量限制,或者当希望队列能够自动扩容以处理更多的任务时。
    • 无界队列(如未指定大小的LinkedBlockingQueue)可以减少任务拒绝的风险,但可能会导致系统资源耗尽,尤其是在任务提交速度远大于处理速度的情况下。
  3. SynchronousQueue - 同步阻塞队列

    • 特点: 这个队列没有实际的容量。每一个插入操作必须等待一个相应的删除操作,反之亦然。
    • 适用场景: 适用于任务处理需要即时响应的场景。例如,newCachedThreadPool就使用这种队列。
    • 同步队列(如SynchronousQueue)适用于任务执行需要立即处理的场景,但可能会对线程池的工作效率产生影响。

ThreadFactory - 创建线程的工厂

ThreadFactory 是一个接口,允许你在Java线程池中自定义线程的创建方式。通过实现ThreadFactory,可以在创建线程时执行一些特定操作,例如自定义线程的名称、优先级、是否为守护线程等。以下是ThreadFactory的一个自定义实现示例:

private static class CustomThreadFactory implements ThreadFactory {
    private final AtomicInteger POOL_NUMBER = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    CustomThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
        namePrefix = "ume-pool-" + POOL_NUMBER.getAndIncrement() + "-thread-";
    }

    @Override
    public Thread newThread(@NonNull Runnable runnable) {
        Thread thread = new Thread(group, runnable, namePrefix + threadNumber.getAndIncrement(), 0);
        if (thread.isDaemon()) {
            thread.setDaemon(false);
        }
        if (thread.getPriority() != Thread.NORM_PRIORITY) {
            thread.setPriority(Thread.NORM_PRIORITY);
        }
        return thread;
    }
}

RejectedExecutionHandler - 拒绝策略

当线程池中的线程数达到最大值且工作队列已满时,RejectedExecutionHandler定义了线程池如何处理无法执行的新提交任务。RejectedExecutionHandler 是一个接口,定义了线程池如何处理这些不能执行的任务。以下是一些标准的实现:

  • AbortPolicy
    • 实现: 这是默认的拒绝策略。当任务被拒绝时,它会抛出RejectedExecutionException异常。
    • 应用场景: 适用于那些希望在任务超出处理能力时立即得知的场合。这种策略可以迅速反馈系统过载的问题。
  • CallerRunsPolicy
    • 实现: 此策略并不抛弃任务,也不抛出异常。相反,它会将任务回退到调用者,从而在调用者的线程中运行任务。
    • 应用场景: 适用于在任务被拒绝时仍希望任务得以执行的场合。这种方式减少了新任务的丢弃,但增加了调用者线程的负载。
  • DiscardPolicy
    • 实现: 任务被拒绝时,该策略将直接丢弃任务,而不会有任何动作。
    • 应用场景: 当任务可以被安全丢弃时使用。这种策略适用于那些对丢弃任务不敏感的场合。
  • DiscardOldestPolicy
    • 实现: 该策略将丢弃最早的未处理任务(即队列中最长时间的任务),然后尝试重新提交新的任务。
    • 应用场景: 适用于希望牺牲部分旧任务以获取新任务处理机会的场景。这种方式试图通过替换旧任务来为新任务腾出空间。
选择合适的策略

在选择合适的RejectedExecutionHandler实现时,需要考虑以下因素:

  • 任务的重要性: 如果每个任务都非常重要,不能被丢弃,那么CallerRunsPolicy可能是一个更好的选择。
  • 资源限制: 如果系统资源(如内存和CPU)非常有限,可能需要选择DiscardPolicyDiscardOldestPolicy来避免资源过载。
  • 性能要求: 如果系统性能是关键考虑因素,AbortPolicy可以迅速反馈系统过载的问题,但可能需要额外的错误处理逻辑。
策略的实现

以下是Java线程池中几种标准拒绝策略的参考代码实现:

  1. AbortPolicy

    这是默认的拒绝策略,抛出RejectedExecutionException异常。

    RejectedExecutionHandler abortPolicyHandler = new ThreadPoolExecutor.AbortPolicy();
    
  2. CallerRunsPolicy

    这种策略将任务回退到调用者的线程中运行。

    RejectedExecutionHandler callerRunsPolicyHandler = new ThreadPoolExecutor.CallerRunsPolicy();
    
  3. DiscardPolicy

    这种策略将直接丢弃任务,而不会有任何动作。

    RejectedExecutionHandler discardPolicyHandler = new ThreadPoolExecutor.DiscardPolicy();
    
  4. DiscardOldestPolicy

    这种策略将丢弃队列中最早的未处理任务,然后尝试重新提交新的任务。

    RejectedExecutionHandler discardOldestPolicyHandler = new ThreadPoolExecutor.DiscardOldestPolicy();
    
  5. 自定义RejectedExecutionHandler

    自定义的拒绝策略可以根据具体需要进行实现,例如下面这个示例记录了被拒绝的任务。

    private static class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
        private CustomRejectedExecutionHandler() {}
    
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            Log.e("umeweb", "Task " + r.toString() + " rejected from " + e.toString());
        }
    }
    

在使用这些策略时,可以将它们作为参数传递给ThreadPoolExecutor的构造函数,以定义线程池如何处理超出其容量和队列限制的任务。

线程池工作原理

假设我们的设置的参数:

  • corePoolSize: 2

  • maximumPoolSize: 4

  • workQueue.size = 2

  1. 最初开始的时候,没有任何线程也没有任何任务添加进任务队列里
image-20240109164743799
  1. 来了一人任务,发现我们的员工还没有达到正式员工数(corePoolSize = 2),来一个员工直接处理这个任务.
image-20240109165159731

又来了一个任务,发现我们的员工还没有达到正式员工数(corePoolSize = 2),再来一个员工直接处理这个任务.

image-20240109165318311

又来了一个任务,但是我们正式员工数已经满了(当前线程数 = corePolSize = 2),任务放到队列(最大长度workQueue.size 是 2) 里等待,而不是再加新员工。

image-20240109165439790

又来了一个任务,但是我们的任务队列已经满了(当前线程数> corePoolSize = 2,已有任务数 = 最大长度workQueue.size = 2) ,新增线程 (maximumPoolSize = 4)来处理新任务,而不是丢弃任务

image-20240109165910222

已经到了任务7,但是我们的任务队列已经满了、临时工也招满了(当前线程数 = maximumPoolSize = 4,已有任务数 = 最大长度,workQueue.size = 2),调用 RejectedExecutionHandler 拒绝策略来处理多余的任务

image-20240109170047531

如果当前线程数超过 corePoolSize (正式员工数),又没有新的任务给他,那么等 keepAliveTime 时间达到后就可以把这个线程释放。

线程池使用方法

此类中定义了线程池的核心参数,包括核心线程数、最大线程数、空闲线程的存活时间、时间单位和任务队列。threadPoolExecutor方法创建并返回一个配置好的ThreadPoolExecutor实例。自定义线程工厂threadFactory用于创建具有特定名称模式的线程,而AbortPolicy作为拒绝策略在任务被拒绝时抛出异常。这些配置确保了线程池的高效和合理管理,同时提供了足够的灵活性来处理各种任务。

package com.caixy.backend.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.validation.constraints.NotNull;
import java.util.concurrent.*;

/**
 * 线程池配置
 *
 * @name: com.caixy.backend.config.ThreadPoolExecutorConfig
 * @author: CAIXYPROMISE
 * @since: 2024-01-09 19:36
 **/
@Configuration
public class ThreadPoolExecutorConfig
{
    // 核心线程数 - 线程池保持活跃的线程数
    private final int corePoolSize = 10;

    // 最大线程数 - 线程池最大能创建的线程数
    private final int maximumPoolSize = 20;

    // 空闲线程存活时间 - 当线程数大于核心线程数时,这是多余空闲线程在终止前的最大存活时间
    private final long keepAliveTime = 60;

    // 时间单位 - 上述存活时间的时间单位
    private final TimeUnit unit = TimeUnit.SECONDS;

    // 队列容量 - 存放待执行任务的队列容量
    private final int queueCapacity = 50;

    // 工作队列 - 用于存放待执行的任务
    private final ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(queueCapacity);

    /**
     * 配置并返回一个ThreadPoolExecutor线程池。
     * 此方法创建一个新的ThreadPoolExecutor并配置其参数
     *
     * @return 配置好的ThreadPoolExecutor实例
     */
    @Bean
    public ThreadPoolExecutor threadPoolExecutor()
    {
        // 自定义线程工厂 - 用于创建新线程
        ThreadFactory threadFactory = new ThreadFactory()
        {
            private int count = 0;

            @Override
            public Thread newThread(@NotNull Runnable r)
            {
                Thread thread = new Thread(r);
                // 设置线程名称
                thread.setName("thread-" + count++);
                return thread;
            }
        };

        // 创建并返回ThreadPoolExecutor实例
        return new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                new ThreadPoolExecutor.AbortPolicy() // 拒绝策略 - 当线程池无法接受任务时抛出异常
        );
    }
}
创建线程池异步任务
@RestController
@RequestMapping("/thread")
@AllArgsConstructor
public class ThreadController
{
    // 注入线程池
 	private final ThreadPoolExecutor threadPoolExecutor;
    
    @GetMapping("/put")
    public String putThread()
    {
        CompletableFuture.runAsync(() -> {
        log.info("{} to do something!!", Thread.currentThread().getName());
            try
            {
                Thread.sleep(1000);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            log.info("{} finished!!", Thread.currentThread().getName());
        }, threadPoolExecutor);
        return "Task submitted successfully";
    }
}

CompletableFuture.runAsync(Runnable runnable, Executor executor) 是Java 8引入的CompletableFuture类的一个方法。它用于异步执行一个任务,这个任务不返回任何值(即Runnable接口的一个实例)。该方法提供了一种在将来某个时间点完成任务执行的方式,而不会阻塞当前线程。以下是该方法的关键特征:

  1. 异步执行: runAsync方法会在一个单独的线程(从指定的Executor中获取)上异步执行给定的Runnable任务。这意味着方法调用会立即返回,而实际的任务执行会在另一个线程中进行。
  2. 任务类型: 作为参数的Runnable接口代表没有返回值的任务。如果您有返回值的异步任务,应该考虑使用CompletableFuture.supplyAsync(Supplier<U>, Executor)
  3. 使用指定的Executor: 您可以指定一个Executor来执行任务,这提供了对执行环境更多的控制。例如,可以指定一个线程池来管理任务的执行。如果不指定Executor,则会使用ForkJoinPool.commonPool()作为默认执行器。
  4. 返回值: 方法返回一个CompletableFuture<Void>对象。这个CompletableFuture可以用来检查任务是否完成(成功或失败),并允许您在任务完成后添加进一步的操作,如使用thenAcceptthenRunexceptionally方法。

结语

如果您喜欢我们的文章,请不要忘记点击关注。我们将继续推出更多关于计算机视觉、人工智能、以及C++、Python、Java等技术领域的精彩内容。您的支持是我们不断前进、分享更多知识和见解的最大动力。我们期待与您一起探索这些激动人心的技术领域,共同成长。感谢您的阅读和支持,敬请期待我们的后续文章!

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

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

相关文章

嵌入式day15

数组指针 能够指向整个数组 一维数组&#xff1a; &a&#xff0c;考察a的数据类型 int&#xff08;*p&#xff09;[10]&#xff1a;表示一个指向长度为10的一维整型数组的指针 二维数组&#xff1a; 指向函数的指针 函数的函数名&#xff0c;即为函数的入口地址&#x…

亲测推荐!PixPin便捷高效,让你的截图工作轻松搞定,还在等什么?

前言 如果你经常使用电脑&#xff0c;是不是也经常遇到这样的烦恼&#xff1a;需要频繁地截图、标注、编辑图片&#xff0c;可是手里的截图工具却总是那么不给力&#xff1f;要么功能单一&#xff0c;要么操作复杂&#xff0c;让人头疼不已&#xff1b;今天咱们的小江湖就要给大…

企业邮箱收发垃圾邮件拦截吗?

企业邮箱如何拦截垃圾邮件呢&#xff1f;企业邮箱拦截垃圾邮件是采用用户定制化和多层防御机制&#xff0c;确保信息安全。用户参与改进系统&#xff0c;提供技术支持。本文详细介绍了企业邮箱过滤垃圾邮件的机制以及企业邮箱的注册流程。 一、企业邮箱面临的垃圾邮件挑战 1、…

对 vllm 与 ollama 的一些研究

今天咱们来聊聊 vllm 和 ollama 这两个听起来就挺酷的玩意儿。这俩都是现在 AI 圈子里的大明星&#xff0c;专门用来让那些超大型的 AI 模型跑得更顺溜。 先说说 vllm 吧&#xff0c;这家伙的绝活儿是剪枝。啥叫剪枝呢&#xff1f;想象一下&#xff0c;你有个花园&#xff0c;…

OpenCV学习笔记 比较基于RANSAC、最小二乘算法的拟合

一、RANSAC算法 https://skydance.blog.csdn.net/article/details/134887458https://skydance.blog.csdn.net/article/details/134887458 二、最小二乘算法 https://skydance.blog.csdn.net/article/details/115413982

基于cubeMX的STM32的模拟SPI驱动的OLED显示

1、OLED的型号为7针SPI接口 2、cubeMX的设置 &#xff08;1&#xff09;GPIO设置 需要注意的是&#xff0c;OLED的五个引脚是普通的IO口来模拟SPI通信&#xff0c;而不是真的用到了单片机的SPI模块。 &#xff08;2&#xff09;时钟设置 最后生成代码。 3、打开工程代码 &a…

如何调节超声驱动的功率和频率

超声波驱动板的功率调节是一个关键的技术操作&#xff0c;它直接影响到超声波设备的运行效果和性能。通过适当的功率调整&#xff0c;可以确保超声波设备在最佳状态下工作&#xff0c;从而提高其效率和应用效果。以下是具体的分析&#xff1a; 理解功率调节基础&#xff1a; 超…

Linux 系统下载 wgent

目录 1. yum 命令 2. 下载 wget 操作系统安装软件的方式有很多种&#xff0c;一般分为&#xff1a; &#xff08;1&#xff09;下载安装包自行安装&#xff1b; &#xff08;2&#xff09;系统的应用商店内安装&#xff1b; Linux 系统同样支持这两种方式&#xff1a; 另…

ShardingSphere实战(3)- 快速实现分库分表

上篇博客&#xff0c;我们讲了 ShardingSphere实战&#xff08;2&#xff09;- 水平分表 &#xff0c;这篇博客&#xff0c;我们继续实现分库以及解决前面遗留的问题。 一、绑定表 基于上篇博客配置的前提下&#xff08;上篇博客的最后放上了完整的配置&#xff0c;需要的可以…

PCI-e(篇一):科普——PCI-e到底是什么?PCI-e的前世今生

一、什么是PCIE接口&#xff1f; PCI-e接口的全称是Peripheral Component Interconnect Express&#xff0c;是一种高速串行计算机扩展总线标准。它原来的名字是“3GIO”&#xff0c;是由Intel在2001年提出的&#xff0c;旨在替代旧的PCI&#xff0c;PCI-X和AGP总线标准。 PC…

SpringBoot使用泛型出入参+策略模式+反射+缓存实现统一POST接口入口

简介 某些情况下需要统一入口&#xff0c;如&#xff1a;提供给第三方调用的接口等。减少接口对接时的复杂性。 代码实现 GenericController.java 统一入口&#xff0c;通过bean name进行调用service层invoke方法 import com.fasterxml.jackson.databind.ObjectMapper; imp…

Python爬虫入门(结合网站代码演示)

原理 第一步发送请求 与浏览器请求访问服务器地址一样&#xff0c;python程序向服务器发送访问请求&#xff0c;服务器返回数据。 在python中我们可以使用 第二步解析网页内容 浏览器在接收到服务器返回的数据后&#xff0c;会自行解析内容最后呈现出我们所看到的界面。但是在程…

Centos7.6安装Nginx(yum安装和源码安装)

Centos7.6安装Nginx&#xff08;yum安装和源码安装&#xff09; 简介 yum安装 源码安装 安装后的事情 常见问题 简介 Nginx&#xff08;发音为“engine X”&#xff09;是一个高性能的HTTP和反向代理服务器&#xff0c;也可以作为邮件代理服务器使用。它被广泛应用于高流量…

linux进程控制——进程等待——wait、waitpid

前言&#xff1a;本节内容仍然是进程的控制&#xff0c;上一节博主讲解的是进程控制里面的进程创建、进程退出、终止。本节内容将讲到进程的等待——等待是为了能够将子进程的资源回收&#xff0c;是父进程等待子进程。 我们前面的章节也提到过等待&#xff0c; 那里的等待是进…

《C++基础入门与实战进阶》专栏介绍

&#x1f680; 前言 本文是《C基础入门与实战进阶》专栏的说明贴&#xff08;点击链接&#xff0c;跳转到专栏主页&#xff0c;欢迎订阅&#xff0c;持续更新…&#xff09;。 专栏介绍&#xff1a;以多年的开发实战为基础&#xff0c;总结并讲解一些的C/C基础与项目实战进阶内…

在MySQL中COUNT(*)、COUNT(列)和COUNT(DISTINCT 列)有什么区别

本文还发布在我的 medium 和 掘金 上 这篇文章让我们看看MySQL中count(*)和count(column_name)有什么区别。也许你知道它们都是计算结果行数的&#xff0c;那么在使用的时候如何选择呢。 我在MySQL库中创建了一个t_hero表 CREATE TABLE t_hero (id int NOT NULL,name char(10)…

软件测试——用例篇(上)

概念 什么是测试⽤例&#xff1f; 测试⽤例&#xff08;Test Case&#xff09;是为了实施测试⽽向被测试的系统提供的⼀组集合&#xff0c;这组集合包含&#xff1a;测试环境、操作步骤、测试数据、预期结果等要素 设计测试⽤例原则⼀&#xff1a; 测试⽤例中⼀个必需部分是对…

AI-WEB-1.0 靶机

AI-WEB-1.0 一、安装靶机环境 下载地址&#xff1a; https://www.vulnhub.com/entry/ai-web-1,353/ 下载压缩文件打开 开启虚拟机 二、信息收集 1.查看NAT模式IP段 编辑–>虚拟网络编辑器 御剑2014查IP 找到ip之后就访问网站 用扫描目录的工具扫描当前网站的目录 访问…

复制知乎文字内容

复制知乎文字内容 以edge浏览器为例&#xff1a; 以edge浏览器为例&#xff1a; 先点击F12&#xff08;也就是鼠标右键->检查&#xff09;再点击F1选择禁用Javascript 之后知乎的文字就可以正常复制了。 &#xff08;注意&#xff1a;退出浏览器的时候记得把这一条恢复过…

XAML 热重载应用程序

XAML 热重载&#xff08;Hot Reload&#xff09;是一个在开发过程中提高效率的功能&#xff0c;它允许开发者在应用程序运行时修改 XAML 代码&#xff0c;而不需要重新启动应用程序。这个功能非常适合于调试和即时预览 UI 更改。以下是如何在应用程序中使用 XAML 热重载的一些基…