理解多线程看这一篇就够了

news2024/10/6 18:26:33

一、基本概念与关系

程序
程序是含有指令和数据的文件,静态地存储在磁盘等存储设备上。它是软件的实体,但未被激活。

进程
进程是程序的一次执行过程,是系统运行程序的基本单位。当程序被操作系统加载并执行时,就成为一个进程,具有动态性。进程拥有独立的内存空间和系统资源(如CPU时间、内存、I/O设备等),是一个正在执行中的程序实例。进程之间相互独立,不共享内存空间。

线程
线程是进程内部的一个执行单元,是进程中实际运作的小单位,也是CPU调度的基本单位。相比进程,线程更轻量,同类线程共享同一块内存空间和系统资源。线程之间可以共享数据,但也可能相互影响,特别是在同一个进程中。线程的引入提高了程序的并发执行能力。

三者之间的关系

  • 程序进程:程序通过加载执行转变为进程,实现了从静态代码到动态执行实体的转变。
  • 进程线程:一个进程可以包含多个线程,这些线程共享进程的资源,在同一进程环境下并发执行不同的任务,提高了效率和响应速度。
  • 线程相比进程,降低了资源开销,提高了通信效率,但同时也可能因为资源共享引发同步问题。

二、线程的基本状态

Java 线程在其生命周期中会经历以下6种基本状态之一,并且随着代码执行和系统调度在这些状态之间转换:

  1. 新建(New):线程刚被创建,尚未启动。
  2. 可运行(Runnable):线程可以被调度执行,包括已经获得CPU时间片正在运行的线程和等待CPU分配时间片的线程。
  3. 阻塞(Blocked):线程等待某个监视器锁(例如进入synchronized代码块时),或者等待I/O操作完成等。
  4. 等待(Waiting):线程等待其他线程执行特定操作,如调用Object.wait()方法,不可被中断直到等待条件满足。
  5. 超时等待(Timed Waiting):与等待状态相似,但线程在指定时间后会自动醒来,如调用Thread.sleep(long millis)方法。
  6. 终止(Terminated):线程已结束执行,无论是正常结束还是异常结束。

这些状态描述了线程从创建到执行完毕的完整生命周期,理解这些状态对于调试多线程程序和避免死锁等问题至关重要。
Java 线程状态如下图所示:
在这里插入图片描述

三、 线程池的原理与创建原因及方式

原理

线程池是一种基于池化思想管理线程的机制,其核心在于重用线程资源,减少创建和销毁线程的开销。线程池的工作流程主要包括以下几个步骤:

  1. 创建线程池:初始化时,预先创建一定数量的线程并将其置于待命状态,等待任务分配。
  2. 任务提交:当有新任务到达时,将其加入到任务队列中。
  3. 任务调度:线程池中的空闲线程会从任务队列中取出任务并执行。若所有线程均在忙状态,且任务队列已满,则根据策略决定是否创建新线程或拒绝任务。
  4. 任务执行完毕:线程不会立即销毁,而是返回线程池等待下一个任务。
  5. 线程管理:根据系统负载动态调整线程数量,避免过多线程导致资源耗尽或过少线程影响任务处理速度。
优点
  1. 资源重用:通过重复利用已创建的线程,减少了线程创建和销毁的开销。
  2. 提高响应速度:任务到达时无需等待新线程创建即可立即执行。
  3. 管理灵活性:可根据系统状况动态调整线程数量,有效控制资源使用,防止资源耗尽。
  4. 简化编程模型:提供统一的接口给开发者提交任务,隐藏了线程管理的复杂细节。
  5. 提高系统稳定性:通过控制最大线程数,防止了过多线程导致的资源竞争和调度开销,增强了系统的稳定性和可预测性。
创建线程池的方式
  1. 手动创建:基础方式,使用语言提供的线程创建API(如Java中的Thread类)配合同步机制(如队列、锁等)自行实现线程池逻辑。

  2. 使用标准库

    • Java: 通过Executors类提供的工厂方法创建不同类型线程池,如newFixedThreadPool创建固定大小线程池,newCachedThreadPool创建可缓存线程池等。
    • C++/POSIX: 利用std::thread结合std::queue等容器自建线程池,或使用第三方库如boost::thread_pool
    • 其他语言:如Python的concurrent.futures.ThreadPoolExecutor,Node.js的worker_threads模块等,都提供了线程池或类似机制的封装。
  3. 第三方库:使用成熟的第三方线程池库,如Java的Apache Commons ThreadPoolExecutor,C#的Microsoft TPL(Task Parallel Library)等,这些库通常提供了更高级的特性,如线程池监控、任务调度策略等。

选择合适的创建方式需根据项目需求、性能要求以及目标平台的支持程度综合考虑。

四、线程池的创建与参数解析

在Java中,通过ThreadPoolExecutor类来创建自定义线程池,其构造函数提供了高度灵活的配置选项,以便根据具体需求调整线程池的行为。下面是构造函数及其参数的详细介绍:

 
public ThreadPoolExecutor(
    int corePoolSize,      // 核心线程数
    int maximumPoolSize,   // 最大线程数
    long keepAliveTime,    // 空闲线程存活时间
    TimeUnit unit,         // 存活时间的单位
    BlockingQueue<Runnable> workQueue, // 任务队列
    RejectedExecutionHandler handler  // 拒绝策略
)
参数解释
  1. corePoolSize核心线程数
    线程池中常驻的核心线程数量。即使线程空闲,这部分线程也会保留在线程池中,不会被回收。只有在工作队列满并且当前线程数小于最大线程数时才会创建新的线程。

  2. maximumPoolSize最大线程数
    线程池能容纳的最大线程数量。当活动线程达到这个数值后,新来的任务如果不能放入任务队列将被拒绝处理。

  3. keepAliveTime空闲线程存活时间
    当线程池中的线程数量超过核心线程数时,多余的空闲线程在等待新任务最长时间达到keepAliveTime后,如果还没有新任务到来,那么这些线程将被终止以节省资源。

  4. unit存活时间的单位
    keepAliveTime参数的时间单位,如TimeUnit.SECONDS秒、TimeUnit.MILLISECONDS毫秒等。

  5. workQueue任务队列
    用于保存等待执行的任务的阻塞队列。不同的队列实现会影响线程池的行为,常见的有ArrayBlockingQueue(固定大小)、LinkedBlockingQueue(无界或有限界限)、SynchronousQueue(直接传递,没有容量)等。

  6. handler拒绝策略
    当线程池和任务队列都达到饱和状态时(即无法再接受新任务),如何处理新提交的任务。JDK内置了几种拒绝策略:

    • AbortPolicy:默认策略,丢弃任务并抛出RejectedExecutionException异常。
    • CallerRunsPolicy:调用者运行策略,直接在调用者线程中执行被拒绝的任务。
    • DiscardPolicy:静默丢弃任务,不抛出任何异常。
    • DiscardOldestPolicy:丢弃队列中最旧的任务,并尝试重新提交当前任务。

通过调整这些参数,可以创建符合特定应用场景需求的线程池,以达到资源最优利用和任务高效执行的目的。

五、 线程池原理

1. 固定大小线程池(FixedThreadPool)

特点

固定大小线程池维护一个固定数量的线程,确保所有任务都会在一个控制好的线程集合中执行,适合于负载较为稳定、任务执行时间相对均衡的场景。由于使用了无界队列LinkedBlockingQueue,如果任务提交速率超过处理能力,队列可能会无限增长,导致内存溢出风险。

参数分析:

  • corePoolSize:等于最大线程数,一旦创建就不会改变,保证线程数量恒定。

  • workQueue:默认为LinkedBlockingQueue,队列容量无上限。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.execute(() -> {
                System.out.println("Task ID : " + taskId + ", Thread Name : " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executor.shutdown();
    }
}
2. 可缓存线程池(CachedThreadPool)

特点:

可缓存线程池会根据任务的需求动态创建线程,空闲线程超过一定时间(默认60秒)则会被回收。这种线程池适合执行大量短期异步任务,能够迅速响应突发请求,但不适合长时间运行的任务,因为线程数量可能无限增长,导致资源耗尽。

参数分析:

  • corePoolSize=0,初始时无核心线程。
  • maximumPoolSize=Integer.MAX_VALUE,理论上允许无限增长。
  • keepAliveTime=1分钟,空闲线程等待新任务的最长时间。
  • workQueue=SynchronousQueue,直接传递,不保留任务。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.execute(() -> {
                System.out.println("Task ID : " + taskId + ", Thread Name : " + Thread.currentThread().getName());
            });
        }
        executor.shutdown();
    }
}
3. 定时任务线程池(ScheduledThreadPool)

特点:

定时任务线程池用于执行定时或周期性的任务,如定时检查、定时清理等。它维护了一个固定大小的核心线程池,并使用DelayedWorkQueue作为任务队列来存放将要执行的任务。

参数分析:

  • corePoolSize:线程池的基本大小,即使没有任务执行,线程也会保持存活。
  • 支持schedule系列方法设定任务的执行时间或周期。
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolDemo {
    public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
        for (int i = 0; i < 3; i++) {
            final int taskId = i;
            executor.scheduleAtFixedRate(() -> {
                System.out.println("Task ID : " + taskId + ", Thread Name : " + Thread.currentThread().getName());
            }, 0, 1, TimeUnit.SECONDS);
        }
    }
}

以上介绍了Java中三种常用的线程池类型,通过实例展示了它们的使用方法和特性。在实际应用中,应根据具体需求选择合适的线程池类型,并考虑是否需要进一步自定义线程池参数,以达到最佳的性能与资源利用率。尽管Executors类提供了简便的工厂方法,但在生产环境中推荐直接使用ThreadPoolExecutor构造函数来实现更细致的控制,以避免潜在的资源耗尽问题

六、常用的线程池

提交一个任务到线程池中,线程池的处理流程如下:

  • 1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创
    建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
  • 2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如
    果工作队列满了,则进入下个流程。
  • 3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
1、ThreadPoolExecutor的execute()方法
public void execute(Runnable command) {
    if (command == null) {
        throw new NullPointerException();
    }
    // 如果线程数大于等于核心线程数或者线程创建失败,将任务加入队列
    if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
        // 线程池处于运行状态并且加入队列成功
        if (runState == RUNNING && workQueue.offer(command)) {
            if (runState != RUNNING || poolSize == 0) {
                ensureQueuedTaskHandled(command);
            }
        } 
        // 线程池不处于运行状态或者加入队列失败,则创建线程(创建的是非核心线程)
        else if (!addIfUnderMaximumPoolSize(command)) {
            // 创建线程失败,则采取阻塞处理的方式
            reject(command); // is shutdown or saturated
        }
    }
}
2、创建线程的方法:addIfUnderCorePoolSize(command)
private boolean addIfUnderCorePoolSize(Runnable firstTask) {
    Thread t = null;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        if (poolSize < corePoolSize && runState == RUNNING) {
            t = addThread(firstTask);
        }
    } finally {
        mainLock.unlock();
    }
    if (t == null) {
        return false;
    }
    t.start();
    return true;
}
我们重点来看第7行:
private Thread addThread(Runnable firstTask) {
    Worker w = new Worker(firstTask);
    Thread t = threadFactory.newThread(w);
    if (t != null) {
        w.thread = t;
        workers.add(w);
        int nt = ++poolSize;
        if (nt > largestPoolSize) {
            largestPoolSize = nt;
        }
    }
    return t;
}

这里将线程封装成工作线程worker,并放入工作线程组里,worker类的方法run方法:

public void run() {
    try {
        Runnable task = firstTask;
        firstTask = null;
        while (task != null || (task = getTask()) != null) {
            runTask(task);
            task = null;
        }
    } finally {
        workerDone(this);
    }
}

worker在执行完任务后,还会通过getTask方法循环获取工作队里里的任务来执行。 我们通过一个程序来观察线程池的工作原理:

3、创建一个线程
public class ThreadPoolTest implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
4、线程池循环运行11个线程:
public static void main(String[] args) {
    LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(5);
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
            5, // 核心线程数
            10, // 最大线程数
            60, // 空闲线程存活时间
            TimeUnit.SECONDS, // 时间单位
            queue // 任务队列
    );

    for (int i = 0; i < 11; i++) {
        threadPool.execute(
                new Thread(new ThreadPoolTest(), "Thread" + i)
        );
        System.out.println("活跃的线程数: " + threadPool.getPoolSize());
        if (queue.size() > 0) {
            System.out.println("----------------队列阻塞的线程数" + queue.size());
        }
    }

    threadPool.shutdown();
}

执行结果:

活跃的线程数: 1
活跃的线程数: 2
活跃的线程数: 3
活跃的线程数: 4
活跃的线程数: 5
活跃的线程数: 5
----------------队列阻塞的线程数1
活跃的线程数: 5
----------------队列阻塞的线程数2
活跃的线程数: 5
----------------队列阻塞的线程数3
活跃的线程数: 5
----------------队列阻塞的线程数4
活跃的线程数: 5
----------------队列阻塞的线程数5
活跃的线程数: 6
----------------队列阻塞的线程数5
活跃的线程数: 7
----------------队列阻塞的线程数5
活跃的线程数: 8
----------------队列阻塞的线程数5
活跃的线程数: 9
----------------队列阻塞的线程数5
活跃的线程数: 10
----------------队列阻塞的线程数5
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task
Thread[Thread15,5,main] rejected from
java.util.concurrent.ThreadPoolExecutor@232204a1[Running, pool size = 10, active
threads = 10, queued tasks = 5, completed tasks = 0]
at
java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPool
Executor.java:2047)
at
java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at
java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at test.ThreadTest.main(ThreadTest.java:17)
分析结果

观察与分析

  1. 线程池配置概览:创建的线程池具体配置为:核心线程数量为5个;最大线程总数为10个;关联的工作队列容量为5个任务。

  2. 工作队列监控:通过queue.size()方法实时监测工作队列中的任务数量,帮助理解线程池的工作状态。

  3. 运行机制解析:

  • 初始阶段,随着任务的提交,线程池会创建核心线程直至达到5个。
  • 当核心线程满载后,新任务被加入到工作队列中,直至队列也达到其容量上限5个。
  • 队列饱和后,线程池会继续创建非核心线程,直至线程总数达到最大限制10个。
  • 若线程数和队列均达到上限,此时线程池进入饱和状态,需依据饱和策略处理后续提交的任务。

饱和策略(RejectedExecutionHandler)调整
当线程池和队列均达到饱和,即无法接纳新任务时,JDK提供了四种预设的饱和策略处理新提交的任务:

  1. AbortPolicy(默认):直接抛出RejectedExecutionException异常。
  2. CallerRunsPolicy:将任务回退给调用线程直接执行。
  3. DiscardOldestPolicy:丢弃队列中最旧的任务,尝试重新提交当前任务。
  4. DiscardPolicy:直接丢弃新提交的任务,不抛出异常。
    修改示例:现在,我们将上述示例中的饱和策略改为DiscardPolicy,即丢弃新提交的任务而不抛出异常。
public static void main(String[] args) {
    // ... 线程池初始化代码保持不变 ...
    
    // 设置饱和策略为DiscardPolicy
    threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());

    // 任务提交逻辑保持不变 ...
    
    threadPool.shutdown();
}

基于之前的讨论,让我们整合并扩展这部分内容,包括修改线程池的饱和策略为DiscardPolicy并进行相应排版:

观察与分析

  1. 线程池配置概览:创建的线程池具体配置为:核心线程数量为5个;最大线程总数为10个;关联的工作队列容量为5个任务。

  2. 工作队列监控:通过queue.size()方法实时监测工作队列中的任务数量,帮助理解线程池的工作状态。

  3. 运行机制解析

    • 初始阶段,随着任务的提交,线程池会创建核心线程直至达到5个。
    • 当核心线程满载后,新任务被加入到工作队列中,直至队列也达到其容量上限5个。
    • 队列饱和后,线程池会继续创建非核心线程,直至线程总数达到最大限制10个。
    • 若线程数和队列均达到上限,此时线程池进入饱和状态,需依据饱和策略处理后续提交的任务。

饱和策略(RejectedExecutionHandler)调整

当线程池和队列均达到饱和,即无法接纳新任务时,JDK提供了四种预设的饱和策略处理新提交的任务:

  1. AbortPolicy(默认):直接抛出RejectedExecutionException异常。
  2. CallerRunsPolicy:将任务回退给调用线程直接执行。
  3. DiscardOldestPolicy:丢弃队列中最旧的任务,尝试重新提交当前任务。
  4. DiscardPolicy:直接丢弃新提交的任务,不抛出异常。

修改示例:现在,我们将上述示例中的饱和策略改为DiscardPolicy,即丢弃新提交的任务而不抛出异常。

public static void main(String[] args) {
    // ... 线程池初始化代码保持不变 ...
    
    // 设置饱和策略为DiscardPolicy
    threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());

    // 任务提交逻辑保持不变 ...
    
    threadPool.shutdown();
}

通过这样的调整,当线程池和队列达到饱和状态后,任何新提交的任务将被默默地丢弃,这种方式适用于那些可以安全丢弃的任务场景,避免了因任务拒绝而引发的异常中断,提高了程序的健壮性。

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

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

相关文章

Kprobe实现原理

kprobe其实就是将某个要检测的指令备份&#xff0c;再替换成int3(x86)或者未定义指令(arm)来触发异常&#xff0c;再调用对应体系的异常处理函数来执行我们自定义的hook&#xff0c;执行完我们自定义的hook,再将备份的指令放回原来的位置继续往下执行 下面我们就来看下linux内核…

拍视频麦克风什么牌子好?户外无线麦克风哪个牌子好,看本期文章

​无线领夹麦克风&#xff0c;作为现代音频技术的重要代表&#xff0c;已经广泛应用于各种场合。它不仅能提高演讲者的声音质量&#xff0c;还能增加演讲的互动性和生动性。然而&#xff0c;面对市场上众多的无线领夹麦克风产品&#xff0c;如何选择一款适合自己的设备成为了一…

TPshop商城的保姆教程(windows)

提前准备 phpStudy下载&#xff1a;https://www.xp.cn/download.html 选择适合自己的版本下载 TPshop商城源文件下载链接&#xff1a; https://pan.baidu.com/s/143fLrxbwe9CTMCbyx7mXJQ?pwd6666 开始安装 安装完phpstudy后 以管理员的身份启动phpstudy.exe 选择合适自己…

C语言 指针——指针变量的定义、初始化及解引用

目录 指针 内存如何编址&#xff1f; 如何对变量进行寻址&#xff1f; 用什么类型的变量来存放变量的地址? 如何显示变量的地址?​编辑 使用未初始化的指针会怎样&#xff1f; NULL是什么&#xff1f; 如何访问指针变量指向的存储单元中的数据&#xff1f; 指针变量的…

二刷算法训练营Day15 | 二叉树(2/9)

目录 详细布置&#xff1a; 1. 层序遍历 2. 226. 翻转二叉树 3. 101. 对称二叉树 详细布置&#xff1a; 1. 层序遍历 昨天练习了几种二叉树的深度优先遍历&#xff0c;包括&#xff1a; ​​​​​​前中后序的递归法前中后序的迭代法前中后序迭代的统一写法 今天&…

糖尿病视网膜病变分级新方法:卷积网络做分割和诊断 + 大模型生成详细的测试和治疗建议

糖尿病视网膜病变分级新方法&#xff1a;卷积网络做分割和诊断 大模型生成详细的测试和治疗建议 提出背景相关工作3.1 数据集3.1.1 病变分割 3.1.2 图像分级3.1.3 大型语言模型&#xff08;LLMs&#xff09; 解法 数据预处理 数据增强 网络架构 训练过程 测试过程子解法1…

spdlog日志库源码:线程池thread_pool

线程池 线程池本质上一组事先创建的子线程&#xff0c;用于并发完成特定任务的机制&#xff0c;避免运行过程中频繁创建、销毁线程&#xff0c;从而降低程序运行效率。通常&#xff0c;线程池主要涉及到以下几个方面问题&#xff1a; 如何创建线程池&#xff1f;线程池如何执…

FPGA基础:触发器和锁存器

目录 锁存器&#xff08;Latch&#xff09;D触发器&#xff08;Flip-Flop&#xff09;最基本时序电路时序块&#xff08;Sequential blocks&#xff09;:同步与异步触发器概念触发器分类触发器的Verilog实现1. 上升沿触发的触发器2. 带异步复位、上升沿触发的触发器3. 带异步复…

Pandas-中axis的用法

在Pandas中&#xff0c;min(axis)方法是计算DataFrame或Series中每行或每列的最小值的函数。该函数可以接受一个参数axis&#xff0c;用于指定计算最小值的方向。当axis0时&#xff0c;表示沿着行的方向计算最小值&#xff1b;当axis1时&#xff0c;表示沿着列的方向计算最小值…

奶奶也能看懂的耦合协调度分析

不会计算&#xff1f;跟着文献学起来~ 案例数据连接&#xff08;复制链接后粘贴到浏览器中&#xff09;&#xff1a; 耦合协调度数据​spssau.com/spssaudata.html?shareDataF363000CD033FF15E557BB75B9B0D412 假如你有这样一组数据&#xff1a; 如何进行计算分析耦合协调度…

python中pow是什么意思

pow()方法返回xy&#xff08;x的y次方&#xff09;的值。 语法 以下是math模块pow()方法的语法&#xff1a; import math math.pow( x, y ) 内置的pow()方法 pow(x, y[, z]) 函数是计算x的y次方&#xff0c;如果z在存在&#xff0c;则再对结果进行取模&#xff0c;其结果等效…

【后端开发】服务开发场景之分布式(CAP,Raft,Gossip | API网关,分布式ID与锁 | RPC,Dubbo,Zookeeper)

【后端开发】服务开发场景之分布式&#xff08;CAP&#xff0c;Raft&#xff0c;Gossip | API网关&#xff0c;分布式ID与锁 | RPC&#xff0c;Dubbo&#xff0c;Zookeeper&#xff09; 文章目录 1、如何设计一个分布式系统&#xff1f;&#xff08;底层原理&#xff09;理论&a…

企业网站有必要进行软件测试吗?网站测试有哪些测试流程?

企业网站在现代商业中扮演着重要的角色&#xff0c;它不仅是企业形象的重要体现&#xff0c;也是与客户、合作伙伴进行沟通与交流的重要渠道。然而&#xff0c;由于企业网站的复杂性和关键性&#xff0c;其中可能存在各种潜在的问题和隐患。因此&#xff0c;对企业网站进行软件…

网页中的音视频裁剪拼接合并

一、需求描述 项目中有一个配音需求&#xff1a; 1&#xff09;首先&#xff0c;前台会拿到一个英语视频&#xff0c;视频的内容是A和B用英语交流&#xff1b; 2&#xff09;然后&#xff0c;用户可以选择为某一个角色配音&#xff0c;假如选择为A配音&#xff0c;那么视频在播…

Centos7时区设置及手动修改时间

一、修改系统时区 1、查看时区命令 timedatectl 2、设置时区命令 #下面将时区设置为上海时区 timedatectl set-timezone Asia/Shanghai 3、查看时区看一下新时区有没有生效 timedatectl 二、手动修改系统时间 修改系统时间 date -s "2023-12-25 16:05:10" 查…

display: none 和 visibility: hidden 的共性与区别

display: none 和 visibility: hidden 的共性与区别 共性&#xff1a;display: none 和 visibility: hidden 都是用于设置元素可见性的样式 区别 display: none 使元素及其占位完全消失&#xff1a;元素及其所有子元素将从文档流和布局中完全消失&#xff0c;就像它们不存在一…

对于高速信号完整性,一块聊聊啊(17)

再来对前仿和后仿的仿真内容回顾一下&#xff1a; 从概念上有个根本的理解 前仿真又可以分为布局前仿真和布局后仿真。前者是在设计的最初阶段&#xff0c;建立和验证详细的电气拓扑结构并以此制定出详细的约束规则。后者是在布局完成的状态下&#xff0c;在布线过程中遇到的…

论文阅读:Correcting Motion Distortion for LIDAR HD-Map Localization

目录 概要 Motivation 整体架构流程 技术细节 小结 论文地址&#xff1a;http://arxiv.org/pdf/2308.13694.pdf 代码地址&#xff1a;https://github.com/mcdermatt/VICET 概要 激光雷达的畸变矫正是一个非常重要的工作。由于扫描式激光雷达传感器需要有限的时间来创建…

python如何安装tar.gz

首先我们到官网下载tar.gz。 然后解压我们下载的pip-9.0.1文件&#xff0c;我的解压后放在d&#xff1a;/p下 运行cmd&#xff0c;输入cd d:\p&#xff0c;按回车键&#xff0c;随后再次输入d: 在d:\p>的光标处输入pip-9.0.1\setup.py install&#xff0c;然后按回车键。 最…

图片转excel表格工具的工具,分享3个专业的识别软件!

在数字化时代&#xff0c;我们时常面临将图片中的表格数据转换为可编辑的Excel表格的需求。无论是工作中的数据整理&#xff0c;还是学习中的资料汇总&#xff0c;这一需求都显得尤为迫切。幸运的是&#xff0c;市面上已经涌现出众多优秀的图片转Excel表格工具&#xff0c;它们…