Java 并发编程基础:线程状态与上下文切换深度解析

主要内容:并发与并行的本质区别、线程生命周期、操作系统任务调度机制,上下文切换。


一、并发和并行的区别

概念 定义 关键特征
并行(Parallel) 多个线程/进程同时执行不同任务 互不干扰,各干各的
并发(Concurrent) 多个线程/进程竞争同一资源 存在资源竞争

二、CPU同一时刻只能执行一条指令

CPU 内部的运算核心(ALU)之间传递的是电压信号。电压信号的特性决定了:

同一导线上,同一时刻只能存在一个电压信号。

如果同时传递两条指令,电压信号会发生串联叠加,CPU 只能接收到一个合并后的错误信号。因此:

  • 单个 CPU 核心,同一时刻只能执行一个任务
  • 多核 CPU可以同时并行执行多个任务
  • 操作系统通过快速切换来模拟同时运行多个任务的效果

三、线程的生命周期

线程从创建到销毁,会经历以下几个状态

新建(New) → 就绪(Runnable) → 运行(Running) → 就绪(Runnable) → ... → 终止(Terminated)
                                     ↓
                                等待(Waiting)

3.1 各状态详解

新建(New)

Thread t1 = new MyThread(); // 仅创建对象,尚未提交给操作系统

此时线程对象在堆内存中存在,但操作系统还不知道它的存在。

就绪(Runnable)

t1.start(); // 不是立刻执行!而是进入就绪队列

start() 的本质是将线程信息提交给操作系统的就绪队列,等待被调度。

⚠️ 很多人误解 start() 是"开始执行",实际上它只是"申请上场资格"。

运行(Running)
操作系统从就绪队列中选中线程,分配 CPU 时间片,线程开始真正执行。

等待(Waiting)
线程主动挂起(如调用 wait()sleep()),让出 CPU,等待被唤醒后重新进入就绪队列。

终止(Terminated)
线程任务执行完毕,从就绪队列中移除,释放资源。

3.2 为什么用户无法让线程直接进入"运行"状态?

因为 CPU 核心可能正在运行其他任务。强行插入会破坏正在执行的任务。操作系统通过"安全抢占"机制,在合适的时机切换任务,保证每个任务不被破坏。


四、任务调度:非公平队列

操作系统的就绪队列并非简单的"先来先得"(公平队列),而是非公平队列

为什么不用公平队列?

  • 纯先进先出会让长任务占用过多时间,用户体验差
  • 短任务先执行完可以快速释放资源,降低队列压力

非公平队列的运作方式:

  1. 按队列顺序依次遍历任务
  2. 判断每个任务是否满足执行条件(优先级、等待时间等)
  3. 第一个满足条件的任务被选中执行
  4. 不满足条件的任务被"放回"队列末尾

它仍然是队列结构(有先后顺序),但允许"插队"——这就是"非公平"的含义。

操作系统会优先调度与用户交互的任务,这也是为什么你双击浏览器图标,浏览器能"秒开"——因为操作系统知道这是用户正在等待的任务,提升了它的优先级。


五、上下文切换(Context Switch)——面试重点

5.1 什么是上下文切换?

CPU 在多个线程之间轮流执行,每次切换时需要:

  1. 保存当前线程执行到哪一步(寄存器状态、程序计数器等)
  2. 加载下一个线程上次保存的执行状态
  3. 从上次中断的地方继续执行

这个"保存 → 加载"的过程就叫上下文切换

5.2 上下文切换的开销

项目 说明
时间开销 毫秒级(每次切换消耗几毫秒)
空间开销 需要额外内存存储线程执行状态
CPU 开销 CPU 需要执行额外指令来完成保存/恢复操作

5.3 操作系统分配的时间片

不同操作系统分配的时间片不同,一般在 几毫秒到几十毫秒 之间:

  • Linux:默认约 4ms(高精度模式)到 100ms
  • Windows:默认约 15ms 左右

5.4 多线程一定比单线程快吗?

不一定。 这是很多人的误区。

多线程的代价:

  • 频繁的上下文切换消耗 CPU 时间
  • 线程创建和销毁本身有开销
  • 线程间的资源竞争和同步机制也有开销

多线程真正的价值在于:充分利用多核 CPU 的并行能力,以及在 I/O 等待期间让 CPU 去执行其他任务(而非空等)。


六、线程内存模型简析

以下面的代码为例,简单理解多线程的内存结构:

public class Main {
    public static void main(String[] args) {
        Thread t1 = new MyThread("线程1");
        Thread t2 = new MyThread("线程2");
        t1.start();
        t2.start();
    }
}

class MyThread extends Thread {
    private String name;
    public MyThread(String name) { this.name = name; }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            System.out.println(name + " - " + i);
            try { Thread.sleep(1); } catch (InterruptedException e) {}
        }
    }
}

内存分布:

方法区(Method Area)
├── main() 方法(静态)
└── MyThread.run() 方法(非静态,每个对象一份)

堆(Heap)
├── t1 对象(MyThread 实例)
└── t2 对象(MyThread 实例)

栈(Stack)—— 每个线程独立一个栈
├── 主线程栈:main() 栈帧 → start() 栈帧
├── t1 线程栈:run() 栈帧 → sleep() 栈帧
└── t2 线程栈:run() 栈帧 → sleep() 栈帧

关键结论:

  • 每个线程拥有独立的栈,互不干扰
  • 堆内存是共享的,这正是并发问题的根源
  • 方法的执行本质是"拷贝入栈 → 执行 → 出栈"的过程

七、其他问题

Q1:并发和并行的区别?

并行是多个线程同时执行互不干扰的任务;并发是多个线程竞争同一资源。

Q2:Thread.start() 和直接调用 run() 的区别?

start() 会将线程提交给操作系统就绪队列,由 OS 调度执行;直接调用 run() 只是普通方法调用,在当前线程中同步执行,不会开启新线程。

Q3:什么是上下文切换?开销是多少?

上下文切换是 CPU 在不同线程间切换时,保存当前线程状态并加载下一个线程状态的过程。时间开销在毫秒级,频繁切换会显著消耗 CPU 资源。

Q4:线程有哪几种状态?

新建(New)、就绪(Runnable)、运行(Running)、等待(Waiting/Timed_Waiting/Blocked)、终止(Terminated)。

Q5:多线程一定比单线程快吗?

不一定。多线程在 I/O 密集型或多核并行场景下有优势,但频繁的上下文切换和同步开销可能使多线程在某些场景下比单线程更慢。


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐