尽管现代计算机多采用多核CPU,多线程通常能带来更高的并发性能,但这并不意味着简单地启动更多线程就能获得更好的效果。过多的线程会增加创建销毁的开销,并引发频繁的上下文切换,反而可能导致程序的吞吐量(TPS)下降。
时间片
在多任务操作系统中,需要同时执行的作业数量常常会超过CPU的核心数。但一个CPU核心在任意时刻只能执行一个任务。那么,如何让用户产生多个任务在同时进行的错觉呢?答案就在于操作系统设计者巧妙采用的时间片轮转机制。
时间片,就是CPU分配给每个任务(线程)执行的一小段时间。
思考:单核CPU究竟是如何支持多线程运行的呢?
线程的上下文包含了某一时刻CPU寄存器和程序计数器的内容。CPU通过时间片分配算法循环执行多个任务(线程)。由于每个时间片都非常短暂,从宏观上看,CPU就像是在多个线程之间快速切换执行。
因此,即使在单核CPU上,通过这种快速的切换也能实现“并发”执行多线程的效果。当然,频繁的切换会带来开销,多核CPU则能在物理层面真正并行执行多个线程,从而在一定程度上减少这种上下文切换。
超线程
现代CPU除了处理器核心,还包含了寄存器、各级缓存、浮点/整数运算单元以及内部总线等组件。多核CPU意味着有多个物理处理器核心。但这也带来了新的挑战:不同核心上运行的线程需要通过外部总线通信,并处理因缓存不一致导致的数据同步问题。
Intel提出的超线程技术,旨在让单个CPU核心能够“同时”处理两个线程。其原理在于,一个线程可能正在使用核心的运算单元,而另一个线程可能正在访问缓存或其他设备,此时两个线程可以并发工作。为了实现这种细粒度的协调,CPU内部增加了一个辅助核心。根据Intel的数据,这一设计大约会增加5%的芯片面积,但能带来15%~30%的性能提升。
上下文切换
上下文切换主要发生在以下几种情况:
- 线程切换:同一进程内的两个线程之间切换。
- 进程切换:两个不同进程之间切换。
- 模式切换:同一线程在用户态和内核态之间切换。
- 地址空间切换:将虚拟内存地址映射切换到物理内存地址。
上下文切换的过程是:CPU在切换任务前,会先将当前任务的状态(上下文)保存下来,以便下次切换回来时能够恢复;然后加载下一个任务的状态并开始执行。这个“保存状态-加载状态”的过程就是上下文切换。
每个线程都拥有自己的程序计数器(PC,指向下一条要执行的指令)、一组寄存器(保存当前工作变量)和一个堆栈(记录调用历史)。
- 寄存器是CPU内部的高速小型存储器,用于快速存取常用值(如运算中间结果),以加速程序运行。
- 程序计数器是一个特殊寄存器,用于指示CPU当前正在执行或接下来要执行的指令在内存中的位置。
一次完整的上下文切换包括以下步骤:
- 挂起当前任务(线程/进程),将其在CPU中的状态(上下文)保存到内存某处。
- 从内存中检索下一个任务的上下文,并将其恢复到CPU的寄存器中。
- 跳转到程序计数器所指向的位置(即任务上次被中断的代码行),恢复执行。

线程上下文切换会有什么问题呢?
上下文切换本身会产生额外开销。这常常表现为,在高并发场景下,使用多线程有时反而比串行执行更慢。因此,减少上下文切换次数是提升多线程程序效率的关键。
- 直接消耗:CPU寄存器需要保存和加载、系统调度器代码需要执行、TLB(快表)需要刷新、CPU流水线(pipeline)需要清空。
- 间接消耗:多核CPU的缓存之间需要同步共享的数据。间接消耗对程序的影响取决于线程操作数据工作集的大小。
切换查看
在Linux系统下,可以使用 vmstat 命令来查看上下文切换的次数。输出结果中的 cs 列就表示上下文切换的数量(通常,空闲系统每秒的上下文切换次数在1500以下)。

线程调度
抢占式调度
在这种调度方式下,每个线程能够执行的时间片长度、以及线程何时切换,都由操作系统内核控制。内核可能给所有线程分配相同长度的时间片,也可能根据优先级等因素进行差异化分配。在这种机制下,一个线程的阻塞不会导致整个进程阻塞。
Java 采用的正是抢占式线程调度。JVM中的线程会按照优先级分配CPU时间片,优先级高的线程通常会获得更多的执行机会,但这并不意味着它能独占CPU,优先级低的线程同样会分到时间片,只是可能更少。

协同式调度
在这种调度方式下,线程在执行完自己的任务后,会主动通知系统将CPU让给其他线程。这种方式就像接力赛跑。线程的执行时间由线程自身控制,切换时机可以预测,也不存在复杂的多线程同步问题。但它有一个致命的缺点:如果一个线程编写不当,在运行中发生阻塞且不释放CPU,就可能导致整个系统崩溃。

线程让出CPU的情况
- 当前运行线程主动放弃CPU,例如调用
yield() 方法。(注意:基于时间片轮转的JVM和操作系统不会让线程永久放弃CPU,通常只是放弃本次时间片)。
- 当前运行线程因为某些原因进入阻塞状态,例如等待I/O操作完成。
- 当前运行线程任务执行完毕,即
run() 方法执行结束。
引起线程上下文切换的因素
- 时间片用完:当前线程的时间片耗尽,系统CPU正常调度下一个就绪线程。
- 中断处理:硬件中断或软件中断“打断”当前执行。CPU响应中断时,会在当前程序与中断处理程序之间进行上下文切换。软件中断原因包括I/O阻塞、未抢到锁资源等。
- 用户态/内核态切换:某些系统调用会引发用户态到内核态的转换,这可能伴随一次上下文切换。
- 锁竞争:多个线程竞争同一锁资源时,未能获取锁的线程可能会被挂起,从而引发上下文切换。
因此优化手段有:
- 无锁并发编程:例如将数据按Hash取模分段,不同线程处理不同段的数据,避免共享资源的锁竞争。
- CAS算法:使用如Java
Atomic 包提供的原子操作,通过Compare-And-Swap机制更新数据,无需加锁。
- 使用最少线程:合理设置线程池大小,避免创建过多闲置线程。
- 协程:在单线程内实现多任务的调度与切换,避免线程切换的开销。
合理设置线程数量是平衡CPU利用率和切换开销的关键。
- 高并发、低耗时:建议使用较少的线程数。
- 低并发、高耗时:建议使用较多的线程数以充分利用CPU。
- 高并发、高耗时:需要具体分析任务类型,可能需结合任务队列、动态调整线程数等策略。
希望这篇关于单核CPU与多线程原理的解析,能帮助你更深入地理解并发编程的底层机制。想了解更多系统性的技术知识,欢迎在 云栈社区 与更多开发者交流探讨。