因为自己对操作系统和底层的知识懂得不是特别深,所以记录一下以下的一些疑问。
Q:
一个springboot项目,部署在6 个核心、12 个线程的处理器上,web请求对应的线程池50个线程,自定义线程池是10个线程,数据库连接池线程30个线程,这不是远远超出了处理器的线程吗?
A:
-
处理器线程(物理核心和逻辑核心) :
- 处理器的核心数和线程数是硬件层面的概念。如 6 个核心、12 个线程,其中物理核心是实际的计算单元,超线程技术使每个物理核心可以同时处理两个线程,相当于把物理核心的资源在逻辑上划分为两份来同时处理不同的线程任务,以此提高处理器的多任务处理能力。
-
应用线程(Web、自定义、数据库连接池线程) :
- 这是软件层面在应用程序中定义的线程。例如,在 Spring Boot 项目中,Web 请求对应的线程池用于处理 HTTP 请求,自定义线程池可能用于处理特定的业务逻辑(如定时任务、消息处理等),数据库连接池线程用于管理数据库连接和执行数据库操作。这些线程池中的线程数量是根据应用程序的功能需求和性能预期来配置的。
-
不同点:
- 资源本质:处理器线程是硬件资源,代表了 CPU 能够同时处理任务的能力;而应用线程是软件层面创建的任务执行单元,它们竞争使用处理器线程资源。
- 数量限制方式:处理器线程数由 CPU 硬件决定,是固定的(在不考虑动态调整技术的情况下);应用线程数量是由开发人员根据应用程序的设计和性能目标在代码中配置的,可以灵活调整。
- 生命周期和管理:处理器线程的生命周期由操作系统和硬件控制;应用线程的生命周期由应用程序的线程池管理,如创建、销毁、调度等操作都由线程池在应用程序的代码逻辑下进行。
-
相同点:
- 目的相同:都是为了执行任务。处理器线程是为了执行操作系统分配的指令流,应用线程是为了执行应用程序中的业务逻辑、I/O 操作等各种任务。
- 相互依赖:应用线程的执行依赖于处理器线程提供的计算资源。只有当处理器线程分配时间片给应用线程时,应用线程才能真正执行任务。
-
联系和协作方式
- 任务调度:操作系统负责将应用线程分配到处理器线程上执行。操作系统使用调度算法(如时间片轮转、优先级调度等)来决定哪个应用线程在哪个处理器线程上执行,以及执行多长时间。例如,当一个 Web 请求线程池中的线程准备执行任务时,操作系统会将其分配到一个空闲的处理器线程或者抢占一个正在执行其他任务的处理器线程的时间片来执行。
- 并发执行和资源竞争:多个应用线程可以并发执行,它们共享处理器线程资源。当应用线程数量超过处理器线程数量时,就会出现资源竞争。例如,50 个 Web 请求线程、10 个自定义线程和 30 个数据库连接池线程总共 90 个线程可能会竞争 12 个处理器线程。在这种情况下,操作系统会频繁地切换线程,让每个线程都有机会执行。但是,过多的线程可能会导致线程切换开销增加,降低系统性能。所以需要合理配置应用线程数量,使其既能充分利用处理器资源,又不会因为过度竞争而导致性能下降。
- 阻塞和唤醒机制:应用线程在执行过程中可能会因为 I/O 操作(如等待网络响应、读取磁盘文件等)而阻塞。当一个线程阻塞时,操作系统会暂停该线程在处理器线程上的执行,并将处理器线程资源分配给其他就绪的线程。当 I/O 操作完成后,线程会被唤醒,重新等待操作系统分配处理器线程资源来继续执行任务。这种机制使得即使应用线程数量很多,在有 I/O 阻塞的情况下,也可以相对高效地利用处理器资源。
Q:
那我如何去理解应用中生成一个线程和销毁一个线程以及线程的上下文切换是一个耗费性能和开销的操作,为什么预先生成一个线程池就可以缓解这种现象?
A:
-
线程的创建和销毁成本
- 资源分配:当创建一个新线程时,操作系统需要为这个线程分配各种资源。这包括为线程的栈空间分配内存,栈空间用于存储线程的局部变量、函数调用信息等。例如,在 32 位系统中,默认的线程栈大小可能是 1MB 左右,在 64 位系统中可能更大。分配这些内存需要系统调用,涉及到内存管理单元(MMU)的操作,这是有一定开销的。
- 初始化操作:新线程创建后,还需要进行一系列的初始化操作。比如,需要初始化线程的寄存器状态,包括程序计数器(PC)、栈指针(SP)等,这些寄存器决定了线程从哪里开始执行以及如何使用栈空间。此外,还可能需要加载一些与线程相关的库和数据结构,这些操作都会消耗 CPU 时间和系统资源。
- 销毁成本:线程销毁时,系统需要回收之前分配给线程的资源。这包括释放栈内存,清理线程相关的数据结构等。如果线程在销毁时还有未完成的 I/O 操作或者资源锁定(如文件锁、互斥锁等),还需要进行额外的清理和解锁操作,以确保系统状态的正确性。
-
线程上下文切换成本
- 状态保存与恢复:当发生线程上下文切换时,操作系统需要保存当前正在执行线程的状态,包括寄存器的值、程序计数器的值等。这些状态信息需要存储到内存中的特定区域(如线程控制块)。然后,操作系统要从另一个线程的存储区域中恢复相应的状态信息,以便新线程能够从上次暂停的地方继续执行。这个保存和恢复的过程涉及到大量的内存读写操作,会消耗 CPU 时间。
- 缓存失效问题:现代处理器都有高速缓存(Cache)来提高数据访问速度。当线程切换时,新线程可能会访问不同的数据,导致原来线程在缓存中的数据可能不再有效,需要重新从内存加载数据到缓存中。这种缓存的重新加载和失效处理也会带来性能开销。
-
线程池如何缓解这些问题
- 复用线程:线程池预先生成了一定数量的线程。当有任务需要执行时,线程池不是创建新的线程,而是从池中取出一个已经存在的空闲线程来执行任务。这样就避免了频繁地创建和销毁线程所带来的资源分配和回收成本。例如,如果每次收到一个 Web 请求都创建一个新线程,在高并发场景下,会频繁地创建和销毁大量线程,而使用线程池可以让这些请求复用已有的线程。
- 减少上下文切换:线程池中的线程数量通常是根据系统资源和应用程序的预期负载来合理设置的。在这个数量范围内,线程之间的切换相对较为有序。因为这些线程在初始化后就一直存在,不需要频繁地进行创建和销毁操作所导致的额外上下文切换。而且,线程池可以根据任务的优先级和等待时间等因素来安排线程执行任务,减少不必要的上下文切换。例如,一个任务执行完后,线程池可以直接将下一个任务分配给刚刚完成任务的线程,而不是让这个线程被其他线程抢占,从而减少了上下文切换的频率。
有助于理解的文章链接: