什么是线程上下文切换?
通常有四种情况会发生线程上下文切换。 第一种是时间片耗尽,操作系统为每个线程分配了一个时间片,当线程的时间片用完后,操作系统会强制切换到其他线程,这是为了保证多个线程能够公平地共享 CPU 资源。 第二种是线程主动让出 CPU,当线程调用了某些方法,如 Thread.sleep()、Object.wait() 或 LockSupport.park()等,会使线程主动让出 CPU,导致上下文切换。 第三种是调用了阻塞类型的系统中断,比如:线程执行 I/O 操作时,由于 I/O 操作通常需要等待外部资源,线程会被挂起,会触发上下文切换。 第四种是被终止或结束运行。 线程上下文切换的过程,分为四步。 第一步是保存当前线程的上下文,将当前线程的寄存器状态、程序计数器、栈信息等保存到内存中。 第二步是根据线程调度算法,如:时间片轮转、优先级调度等,选择下一个要运行的线程。 第三步是加载下一个线程的上下文,从内存中恢复所选线程的寄存器状态、程序计数器和栈信息。 第四步是 CPU 开始执行被加载的线程的代码。
定义
- 线程上下文切换(Thread Context Switch):
- 是操作系统在多线程执行时,将 CPU 从一个线程切换到另一个线程的过程,涉及保存当前线程的状态并加载新线程的状态。
核心点
- 目的:实现多线程并发。
- 代价:切换涉及时间和资源开销。
1. 线程上下文切换详解
(1) 基本概念
- 上下文:
- 线程运行时的状态,包括:
- 寄存器:如程序计数器(PC)、栈指针。
- 线程栈:局部变量、调用栈。
- 状态:如运行、等待。
- 切换过程:
- CPU 暂停当前线程,保存上下文到内存(PCB,进程控制块或线程控制块)。
- 加载新线程的上下文到 CPU,继续执行。
(2) 触发场景
- 时间片用尽:
- 多线程抢占式调度,时间片到期。
- 线程阻塞:
- 如等待 I/O、锁、调用
sleep()
。 - 线程优先级:
- 高优先级线程抢占。
- 手动让出:
- 调用
Thread.yield()
。
(3) 执行步骤
- 保存当前线程:
- 寄存器值(如 PC、SP)存入线程的栈或 PCB。
- 调度新线程:
- 操作系统选择就绪队列中的线程。
- 加载新线程:
- 将新线程的上下文恢复到寄存器。
- 切换执行:
- CPU 开始执行新线程代码。
2. 开销与影响
(1) 时间开销
- 直接开销:
- 保存和加载上下文,约几微秒到几十微秒(视硬件和 OS)。
- 间接开销:
- 缓存失效(CPU Cache 未命中)。
- TLB(翻译后备缓冲器)刷新。
(2) 系统影响
- 性能:
- 频繁切换降低吞吐量。
- 响应性:
- 适度切换提升多任务并行。
示例
- 线程 A 执行中,调用
sleep(1000)
: - A 的上下文保存。
- 切换到线程 B,B 执行。
- 1 秒后,A 恢复。
3. Java 中的体现
(1) JVM 与 OS
- Java 线程由 JVM 管理,映射到操作系统原生线程。
- 上下文切换由 OS 内核完成,JVM 无直接控制。
(2) 相关方法
Thread.sleep()
:- 触发切换,进入 Timed Waiting。
Thread.yield()
:- 让出 CPU,回到 Runnable。
wait()
:- 等待时切换至其他线程。
(3) 查看切换
- 工具:
- JVisualVM、Thread Dump 分析线程状态。
- 指标:
- 上下文切换次数(Linux:
vmstat
的cs
列)。
4. 延伸与面试角度
- 与进程切换对比:
- 线程切换:同一进程内,共享内存,较轻量。
- 进程切换:不同进程,切换地址空间,开销大。
- 优化策略:
- 减少线程数:用线程池。
- 避免频繁阻塞:异步 I/O。
- 调整优先级:减少抢占。
- 实际应用:
- 高并发:Web 服务器线程切换频繁。
- 实时系统:尽量减少切换。
- 面试点:
- 问“定义”时,提保存和加载。
- 问“影响”时,提开销和优化。
总结
线程上下文切换是 OS 为多线程分配 CPU 的过程,保存当前线程状态并加载新线程,开销包括时间和缓存。Java 中由 OS 控制,影响性能和响应性。面试时,可提触发场景或优化方法,展示理解深度。