当前位置: 首页 > backend >正文

多线程入门到精通系列: 从操作系统到 Java 线程模型

目录

一、进程与线程:操作系统视角的本质区别

1.1 进程:资源分配的基本单位

1.2 线程:CPU 调度的基本单位

二、线程调度:操作系统如何管理线程

2.1 时间片轮转调度

2.2 上下文切换的代价

三、Java 线程模型:JVM 如何映射操作系统线程

3.1 1:1 线程模型

3.2 为什么 Java 不采用用户态线程?

四、Java 线程的状态:从源码看线程生命周期

五、实战:用工具观察线程状态

5.1 代码示例:创建不同状态的线程

5.2 用 jstack 查看线程状态

六、新手常见误区与最佳实践

6.1 误区 1:线程越多,程序越快

6.2 误区 2:直接调用 run () 方法启动线程

6.3 误区 3:用 stop () 方法终止线程

七、总结与下一篇预告


一、进程与线程:操作系统视角的本质区别

要理解线程,必须先搞懂进程。无论是 Windows、Linux 还是 macOS,现代操作系统的核心调度单位都是进程和线程,它们的设计源于一个朴素的需求:让计算机同时 "做" 多件事

1.1 进程:资源分配的基本单位

进程(Process)是程序的一次执行过程。当你双击打开浏览器,操作系统就会创建一个进程

  • 它会占用一块独立的内存空间(代码、数据、堆等)
  • 会申请文件句柄、网络端口等系统资源
  • 拥有自己的程序计数器(记录下一条要执行的指令)

比如打开macos的程序监控,可以看到管理打开软件的基本单位是进程

进程的关键特性是资源隔离:一个进程崩溃通常不会影响其他进程(比如浏览器崩溃不会导致微信关闭)。但这种隔离是有代价的 —— 创建进程、切换进程的开销很大(涉及内存映射、资源分配等)。

1.2 线程:CPU 调度的基本单位

线程(Thread)是进程内的执行单元,它共享进程的资源(内存、文件句柄等),但有自己的栈和程序计数器。

为什么需要线程?举个例子:当你用浏览器下载文件时,还能同时滚动页面 —— 这就是多线程的作用。如果用多进程实现,不仅资源占用大,进程间通信也很麻烦。

进程与线程的核心区别:

  • 进程是资源分配的单位,线程是 CPU 调度的单位

  • 进程间资源隔离,线程间资源共享

  • 进程创建 / 切换成本高,线程创建 / 切换成本低(约为进程的 1/10 到 1/100)


二、线程调度:操作系统如何管理线程

线程创建后,由操作系统的调度器(Scheduler)负责分配 CPU 时间。

2.1 时间片轮转调度

现代操作系统基本都采用时间片轮转(Round-Robin)调度策略:

每个线程被分配一个时间片(通常 10-100 毫秒)

时间片用完后,CPU 切换到下一个线程(上下文切换)

所有线程轮流使用 CPU,宏观上看起来 "同时执行"

2.2 上下文切换的代价

当 CPU 从一个线程切换到另一个线程时,需要保存当前线程的状态(寄存器、程序计数器等),并加载新线程的状态 —— 这个过程称为上下文切换(Context Switch)。

上下文切换的代价往往被低估:

一次切换大约消耗 1-10 微秒(具体取决于硬件)

如果线程数过多,CPU 会把大部分时间花在切换上,而非执行任务


三、Java 线程模型:JVM 如何映射操作系统线程

Java 线程(java.lang.Thread)并不是凭空存在的,它依赖于底层操作系统的线程实现。

3.1 1:1 线程模型

目前主流的 JVM(如 HotSpot)都采用1:1 线程模型

一个 Java 线程对应一个操作系统线程(OS Thread)

JVM 负责将 Java 线程的操作(如 start、sleep)映射到 OS 线程的系统调用

线程调度完全由操作系统负责,JVM 无法干预

这种模型的优势是实现简单,能充分利用操作系统的调度能力;缺点是创建线程的成本较高(受限于 OS 线程的创建成本)。

3.2 为什么 Java 不采用用户态线程?

有些语言(如 Go)采用用户态线程(如 Goroutine),由语言 runtime 而非操作系统调度,能创建百万级线程。Java 早期也尝试过(如 JDK 1.1 的 Green Threads),但最终放弃,主要原因是:

  1. 操作系统调度更成熟:现代 OS 的线程调度已经过几十年优化,能充分利用多核 CPU

  2. 兼容原生库:很多 Java 库(如数据库驱动、网络库)依赖操作系统的阻塞 IO,用户态线程难以适配

  3. 开发复杂度高:实现高效的用户态线程调度器非常复杂

不过 Java 正在迎头赶上 ——Java 21 引入的虚拟线程(Virtual Threads)就是一种轻量级用户态线程,后面会专门讲解。

四、Java 线程的状态:从源码看线程生命周期

Java 线程的状态定义在Thread.State枚举中,共 6 种状态。理解这些状态,是排查线程问题的基础。

public enum State {NEW,          // 新建:线程已创建但未调用start()RUNNABLE,     // 可运行:包含操作系统的运行中和就绪状态BLOCKED,      // 阻塞:等待获取监视器锁WAITING,      // 等待:无超时等待其他线程通知TIMED_WAITING,// 计时等待:有超时的等待TERMINATED    // 终止:线程执行完毕
}

状态转换的关键节点:

NEW → RUNNABLE:调用start()方法(注意不是run()

Thread t = new Thread();
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE(大概率)

RUNNABLE → BLOCKED:获取synchronized锁失败时

进入 BLOCKED 状态,等待锁释放

RUNNABLE → WAITING:调用Object.wait()Thread.join()等方法

必须等待其他线程调用notify()/notifyAll()才能唤醒

RUNNABLE → TIMED_WAITING:调用Thread.sleep(long)Object.wait(long)

无需其他线程通知,超时后自动唤醒

所有状态 → TERMINATEDrun()方法执行完毕或抛出未捕获异常

注意:Java 的RUNNABLE状态包含两种情况:

  • 线程正在 CPU 上执行
  • 线程处于就绪状态,等待 CPU 调度

这一点与操作系统的线程状态不同,需要特别注意。

五、实战:用工具观察线程状态

理论讲完了,我们用实际代码和工具来观察线程状态。

5.1 代码示例:创建不同状态的线程

public class ThreadStateDemo {public static void main(String[] args) throws InterruptedException {// 1. NEW状态Thread newThread = new Thread(() -> {});System.out.println("newThread状态: " + newThread.getState());// 2. RUNNABLE状态Thread runnableThread = new Thread(() -> {while (true) { // 无限循环,保持运行状态}});runnableThread.start();Thread.sleep(100); // 等待线程启动System.out.println("runnableThread状态: " + runnableThread.getState());// 3. BLOCKED状态Object lock = new Object();Thread blockedThread1 = new Thread(() -> {synchronized (lock) {try {Thread.sleep(10000); // 持有锁并休眠} catch (InterruptedException e) {e.printStackTrace();}}});blockedThread1.start();Thread.sleep(100); // 确保blockedThread1先获取锁Thread blockedThread2 = new Thread(() -> {synchronized (lock) { // 尝试获取已被持有的锁System.out.println("blockedThread2获取到锁");}});blockedThread2.start();Thread.sleep(100);System.out.println("blockedThread2状态: " + blockedThread2.getState());// 4. WAITING状态Thread waitingThread = new Thread(() -> {synchronized (lock) {try {lock.wait(); // 无超时等待} catch (InterruptedException e) {e.printStackTrace();}}});waitingThread.start();Thread.sleep(100);System.out.println("waitingThread状态: " + waitingThread.getState());// 5. TIMED_WAITING状态Thread timedWaitingThread = new Thread(() -> {try {Thread.sleep(10000); // 计时等待} catch (InterruptedException e) {e.printStackTrace();}});timedWaitingThread.start();Thread.sleep(100);System.out.println("timedWaitingThread状态: " + timedWaitingThread.getState());// 6. TERMINATED状态Thread terminatedThread = new Thread(() -> {});terminatedThread.start();Thread.sleep(100); // 等待线程执行完毕System.out.println("terminatedThread状态: " + terminatedThread.getState());// 销毁所有线程(示例用,实际开发不建议)runnableThread.interrupt();blockedThread1.interrupt();waitingThread.interrupt();timedWaitingThread.interrupt();}
}

运行结果(可能因环境略有差异):

newThread状态: NEW
runnableThread状态: RUNNABLE
blockedThread2状态: BLOCKED
waitingThread状态: WAITING
timedWaitingThread状态: TIMED_WAITING
terminatedThread状态: TERMINATED

5.2 用 jstack 查看线程状态

jstack 是 JDK 自带的工具,能打印 Java 进程的线程栈信息,是排查线程问题的利器。

1.先找到程序的进程 ID(PID):

jps -l

输出类似:

12345 ThreadStateDemo

2. 用 jstack 打印线程信息:

jstack 12345

3.在输出中找到我们创建的线程,例如 BLOCKED 状态的线程:

"Thread-2" #12 prio=5 os_prio=31 tid=0x00007f8a1a0a000 nid=0x5a03 waiting for monitor entry [0x000070000f9f3000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at ThreadStateDemo.lambda$main$2(ThreadStateDemo.java:35)
    - waiting to lock <0x000000076ab36f80> (a java.lang.Object)
    at ThreadStateDemo$$Lambda$3/0x0000000800060840.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:833)

通过 jstack,你可以清晰地看到线程的状态、等待的锁、调用栈等信息,这在排查死锁、线程阻塞等问题时非常有用


六、新手常见误区与最佳实践

6.1 误区 1:线程越多,程序越快

最佳实践:根据任务类型设置合理的线程数:

  • CPU 密集型任务:线程数 ≈ CPU 核心数

  • IO 密集型任务:线程数 ≈ CPU 核心数 * 2(或根据 IO 等待时间调整)

6.2 误区 2:直接调用 run () 方法启动线程

很多新手会犯这样的错误:

Thread t = new Thread(() -> System.out.println("运行中"));
t.run(); // 错误:直接调用run()不会启动新线程

正确做法:调用start()方法,它会通知 JVM 创建新线程并执行run()

t.start(); // 正确:启动新线程

6.3 误区 3:用 stop () 方法终止线程

Thread.stop()已被废弃,因为它会强制终止线程,可能导致资源未释放、数据不一致等问题。

正确做法:用interrupt()配合标志位终止线程:

Thread t = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {// 执行任务}System.out.println("线程安全终止");
});
t.start();// 终止线程
t.interrupt();

七、总结与下一篇预告

本文从操作系统底层到 Java 线程模型,讲解了线程的本质:

线程是 CPU 调度的基本单位,比进程更轻量

Java 采用 1:1 线程模型,线程状态与操作系统状态有映射关系

线程不是越多越好,上下文切换有性能代价

理解这些基础,是掌握多线程编程的前提。下一篇《多线程入门到精通系列: JDK源码理解3种方式与核心API》,我们将详细讲解 Java 创建线程的 3 种方式,以及start()join()sleep()等核心方法的正确用法,敬请期待。

http://www.xdnf.cn/news/19916.html

相关文章:

  • 快鹭云业财一体化系统技术解析:低代码+AI如何破解数据孤岛难题
  • 飞算JavaAI开发在线图书借阅平台全记录:从0到1的实践指南
  • 【C++】详解形参和实参:别再傻傻分不清
  • Android adb shell命令分析应用内存占用
  • 2025全国大学生数学建模C题保姆级思路模型(持续更新):NIPT 的时点选择与胎儿的异常判定
  • Trae + MCP : 一键生成专业封面——从概念到落地的全链路实战
  • java对接物联网设备(一)——使用okhttp网络工具框架对接标准API接口
  • SVN和Git两种版本管理系统对比
  • Hunyuan-MT-7B模型介绍
  • 使用Vue.js和WebSocket打造实时库存仪表盘
  • window使用ffmep工具,加自定义脚本执行视频转码成h264(运营人员使用)
  • P13929 [蓝桥杯 2022 省 Java B] 山 题解
  • 第三方网站测评:【WEB应用文件包含漏洞(LFI/RFI)的测试步骤】
  • 神经网络模型介绍
  • LeetCode 3132.找出与数组相加的整数2
  • 机器学习算法在Backtrader策略稳定性中的作用分析
  • pytorch可视化工具(训练评估:Tensorboard、swanlab)
  • c#编写的应用程序调用不在同一文件夹下的DLL
  • OpenLayers 入门篇教程 -- 章节三 :掌控地图的视野和交互
  • 下一代自动驾驶汽车系统XIL验证方法
  • 【Doris入门】Doris数据表模型使用指南:核心注意事项与实践
  • select, poll, epoll
  • PyTorch 损失函数与优化器全面指南:从理论到实践
  • 论文理解:Reflexion: Language Agents with Verbal Reinforcement Learning
  • 【正则表达式】 正则表达式运算法优先级的先后是怎么排序的?
  • 【Pytest】解决Pytest中Teardown钩子的TypeError:实例方法与类方法的调用差异
  • Java中最常用的设计模式
  • Mysql主从复制之延时同步
  • 【Linux基础】Linux系统管理:深入理解Linux运行级别及其应用
  • 面经分享二:Kafka、RabbitMQ 、RocketMQ 这三中消息中间件实现原理、区别与适用场景