一:什么是线程
线程: 进程中的一条执行流, 每个进程一开始(启动之后)都会有一个"主线程"
前提: 一个 进程 中 可以有多个线程
(1) 线程直接共享进程的所有资源(如:mm_struct), 创建线程比创建进程要快10~100倍
(2) 线程之间共享相同的地址空间(mm_struct), 这样利于线程之间数据高效的传输
(3) 多CPU操作系统中, 多个线程可以真正的并行执行
比如:下图是进程A中有三个线程
比如:下图是日常分析问题的Log日志, 从Log中我们也能看得出进程中有很多线程
从两个角度来重新认识进程: 
二:Linux操作系统中线程的创建
Linux操作系统中依然使用 "task_struct" 数据结构来维护一个线程
下图演示了Linux操作系统中 线程 的创建过程 
fork 与 clone 的区别

三:用户级线程 vs 内核级线程
TCB: Thread Control Block(线程控制块)
PCB: Process Control Block(进程控制块)
线程的实现(创建)方式主要有两种,分别是:"用户级线程"、 "内核级线程"
| 用户级线程 | 内核级线程 |
| 在用户态实现 | 在内核态实现 |
| 用户态直接创建并维护 | 用户态发起创建--->透过--->系统调用--->内核态真正新建线程 |
| ------------------------------------ | POSIX Threads (pthreads) 库、 JAVA Thread库 |
用户级线程:在用户空间实现的线程,操作系统内核看不到的线程(线程的TCB存放在用户空间的,操作系统内核它只能看到进程_PCB, 操作系统只能以进程为单位来调度) ---------》用户级线程是由一些应用程序中的线程库()来实现,应用程序可以调用"线程库"的API来完成线程的创建、线程的结束、等操作
内核级线程:在内核空间实现的线程,由操作系统管理的线程---------》内核级线程管理的所有工作都是由操作系统内核完成的, 比如内核线程的创建,结束,是否占用CPU等。都是由操作系统内核来管理
用户级线程 特点 & 缺点:

内核级线程 特点 & 缺点:
内核级线程的创建,终止、切换都是由内核来完成的,所以应用程序如果想用内核级线程的话,需要通过系统调用来完成内核级线程的创建、终止、切换。这里会涉及到用户态和内核态的转换,因此相对于前面用户级线程,"系统开销大" 
如今各类操作系统中线程模型
多对1 模型 
1对1 模型 
多对多 模型 
四:Linux内核程序是怎样创建线程的
内核启动的时候,初始化init_mm, 它全局只有一个,管理内核程序虚拟地址空间
进程 task_struct 中的 active_mm
内核线程的创建,使用 kernal_thread() 
五:线程的状态
(A): 线程的生命周期
每种操作系统,每种编程语言,都有自己的线程生命周期控制,大部分满足上面的线程生命周期,但也都各有不同。
a.1 在Java中,线程状态是明确定义在 java.lang.Thread.State 枚举中的,共有以下 6种 状态:
| 状态名称 | 说明 |
| NEW | 线程被创建,但尚未启动(即未调用 start()方法) |
| RUNNABLE | 线程正在Java虚拟机中运行,或正在等待操作系统资源(如CPU时间片) |
| BLOCKED | 线程被阻塞,等待获取一个监视器锁(如进入 synchronized块) |
| WAITING | 线程无限期等待另一个线程执行特定操作(如 Object.wait()、join()) |
| TIMED_WAITING | 线程在指定时间内等待另一个线程执行操作(如 sleep()、wait(timeout)) |
| TERMINATED | 线程已执行完毕或被中断,生命周期结束 |
a.2 C++ 中的线程状态(以 C++11 及以后为标准)
C++11 引入了 标准库,但 C++ 标准本身并不定义线程的内部状态枚举,线程状态由底层操作系统管理。不过,从使用角度可以归纳出以下 常见状态:
| 状态名称 | 说明 | |
| 创建(Created) | 线程对象已创建,但尚未调用 join()或 detach(),可能尚未启动 | |
| 运行(Running) | 线程正在执行其入口函数 | |
| 阻塞(Blocked) | 线程因等待互斥锁、条件变量、I/O 等而暂停 | |
| 等待(Waiting) | 线程因调用 std::this_thread::sleep_for、condition_variable::wait等而挂起 | |
| 完成(Finished) | 线程入口函数执行完毕,但资源可能尚未释放(取决于是否 join()) | |
| 可合并/分离(Joinable / Detached) | 线程是否可被 join(),或已调用 detach()成为后台线程 |
⚠️ 注意:C++ 标准库没有提供查询线程状态的接口(如 getState()),状态信息依赖于操作系统(如 Linux 的 /proc、Windows 的线程句柄状态)。
a.3 C 中的线程状态(以 POSIX threads - pthread 为例)
C 语言本身没有线程概念,线程支持依赖于平台库,最常用的是 POSIX threads(pthread)。pthread 也没有官方枚举状态,但可以从线程生命周期中归纳出以下 常见状态:
| 状态名称 | 说明 |
| 创建(Created) | 线程已调用 pthread_create(),但尚未开始执行或尚未被调度 |
| 运行(Running) | 线程正在执行其入口函数 |
| 阻塞(Blocked) | 线程因等待互斥锁(pthread_mutex_lock)、条件变量(pthread_cond_wait)等而阻塞 |
| 等待(Waiting) | 线程因调用 sleep()、pthread_cond_timedwait等而挂起 |
| 终止(Terminated) | 线程入口函数返回或调用 pthread_exit(),但资源尚未释放(未 join) |
| 已分离(Detached) | 线程已调用 pthread_detach(),资源在线程结束时自动回收 |
⚠️ 同样,pthread 没有提供获取线程状态的函数(如 pthread_getstate()),状态信息需通过调试工具(如 gdb、top、htop、ps -L)查看。
总结对比表
| 语言/库 | 是否明确定义线程状态 | 状态数量 | 是否可查询状态 | 备注 |
| Java | ✅(Thread.State) | 6种 | ✅ getState() | 标准明确,跨平台一致 |
| C++ 11+ | ❌(标准未定义) | 约5~6种 | ❌ 无标准接口 | 状态依赖于操作系统 |
| C (pthread) | ❌(库未定义) | 约5~6种 | ❌ 无标准接口 | 状态依赖于操作系统 |
如需进一步查看底层线程状态(如 Linux 的 task_state、Windows 的 THREAD_STATE),可结合系统工具或调试器分析。
a.4 Linux 操作系统内核中,线程和进程使用相同的状态描述
📊 Linux 线程(进程)状态一览
下面的表格汇总了 Linux 中主要的线程(进程)状态 "下面总结的不是全部状态,还有很多哈,AI了解一下"、 它们在 ps 或 top 命令中对应的显示字母以及简单的说明:
| 状态宏定义 | 在 ps/top 中的显示 | 说明 |
| TASK_RUNNING | R | 线程正在运行或就绪(在运行队列中等待调度)。 |
| TASK_INTERRUPTIBLE | S | 可中断的睡眠[阻塞]。线程在等待某个事件(如I/O完成、信号量),可被信号或中断唤醒。 |
| TASK_UNINTERRUPTIBLE | D | 不可中断的睡眠[阻塞]。线程在等待某些特定条件(通常是I/O),不会响应信号(即使是 kill -9)。此状态通常很短暂。 |
| TASK_STOPPED | T | 线程被暂停,例如通过 SIGSTOP信号。 可以通过 SIGCONT信号恢复运行。 |
| TASK_TRACED | T | 线程正在被跟踪(例如由调试器 gdb 在断点处暂停)。与 TASK_STOPPED类似,但多了一层保护,不能通过 SIGCONT信号恢复。 |
| EXIT_ZOMBIE | Z | 僵尸状态。线程已终止,但其退出状态信息还未被父进程获取(例如父进程未调用 wait())。此时线程占用的绝大多数资源已释放,仅保留 task_struct空壳。 |
| EXIT_DEAD | X | 死亡状态。线程的最终状态,接下来会彻底被系统销毁回收。此状态非常短暂,通常无法通过 ps命令捕捉到。 |
🔍 如何查看线程状态
你可以使用以下命令来查看线程的状态:
css
- ps 命令:例如 ps aux 或 ps -eLF,在 STAT 或 S 列查看状态字母。
- top 命令:运行 top 后,在 S 列查看状态字母。按下 H 键可以切换至线程视图。
💎 简单总结
理解这些状态对看BUG问题很有帮助。比如,偶尔看到 D 状态通常是正常的,但如果多个线程长时间处于 D 状态,可能意味着某些硬件(如磁盘)或驱动出现了问题。而大量的 Z 状态线程则会消耗系统进程号资源,是需要避免的"僵尸线程"。
(B): Linux操作系统中线程的状态变化
linux的线程状态存放在 该线程的 task_struct 中
注意:linux中的线程,没有就绪状态(在linux中,就绪状态和运行状态的线程都是 "TASK_RUNNING", 但是linux搞了一个专门用来指向当前运行任务的指针current 指向它,以表示它是一个正在运行的线程)
下图展示了部分状态转换过程: 
(1) TASK_INTERRUPTIBLE 可中断的阻塞(睡眠)状态: 阻塞(睡眠)的时候,会响应其他信号(如: kill) 
(2) TASK_UNINTERRUPTIBLE 不可中断的阻塞(睡眠)状态: 阻塞(睡眠)的时候,忽略其他信号

跑个题:了解一些 I/O 知识
I/O 操作的核心是"输入/输出",它的本质是 数据在 CPU/内存 与 外部设备之间 的流动。 磁盘读写只是其中最常见、最典型的一种,但远不是全部。
可以把 CPU 和内存想象成公司的"总部办公室",而所有其他设备都是"外部办事处或合作伙伴"。任何需要与这些"外部实体"进行的数据交换,都属于 I/O 操作。
以下是操作系统中主要的 I/O 操作类型:
1. 存储设备 I/O(你最熟悉的)
这是与持久化存储设备的数据交换。
- 磁盘 I/O:读写硬盘(HDD)、固态硬盘(SSD)。例如:保存文件、加载程序、数据库查询。
- 光盘 I/O:从 CD、DVD、蓝光光盘读取数据。
- U盘/移动硬盘 I/O。
2. 网络 I/O
这是与网络设备(网卡)的数据交换,是网络编程和互联网应用的基石。
- 发送数据:将数据包通过网卡发送到网络上。
- 接收数据:从网卡读取到来的数据包。
- 例子:浏览网页、发送邮件、在线视频、网络游戏。这通常不被用户直观地感知为"I/O",但它在系统资源占用和性能上极其重要。
3. 外设 I/O
这是与用户交互或专用功能设备的通信。
- 键盘输入:你按下按键,对于 OS 来说就是一个输入操作。
- 鼠标输入:移动鼠标、点击按键,也是一系列输入操作。
- 显示器输出:GPU 将渲染好的帧数据输出到显示器。
- 打印机输出:将文档数据发送给打印机。
- 扫描仪输入:从扫描仪获取图像数据。
- 音响/耳机输出:声卡将数字音频信号输出到音响设备。
- 麦克风输入:声卡从麦克风录制音频信号。
4. 进程间通信(IPC - Inter-Process Communication)
这是在同一台机器上,不同进程之间交换数据。虽然数据可能没有离开主机,但它跨越了进程的"边界",所以也是一种 I/O。
- 管道:一个进程的输出作为另一个进程的输入(如 shell 中的 | 操作符)。
- 消息队列
- 共享内存
一个重要的概念:为什么 I/O 很"慢"?
理解 I/O 的关键在于认识到它与 CPU 处理速度的巨大差异。
-
CPU 和 内存(纳秒级):处理速度极快,在纳秒级别。
-
I/O 设备(毫秒级甚至秒级):
- 机械硬盘寻道需要毫秒级。
- 网络请求可能需要几十到几百毫秒。
- 等待用户键盘输入可能需要几秒甚至几分钟。
这个速度差距是几个数量级的! 因此,如何高效地管理 I/O,避免让高速的 CPU 白白等待低速的 I/O 设备,就成了操作系统设计中的一个核心问题。这就引出了以下几种 I/O 模型:
操作系统中的五种主要 I/O 模型
这些模型定义了应用程序如何与操作系统协作来完成一个 I/O 操作。
-
阻塞 I/O :
- 应用发起 I/O 调用后,线程被挂起,一直等待数据准备好并从内核缓冲区拷贝到用户空间后,才继续执行。
- 最简单,但性能最差,一个线程只能处理一个 I/O 流。
-
非阻塞 I/O :
- 应用发起 I/O 调用后,立即返回一个错误码,不会阻塞线程。
- 应用程序需要不断地轮询内核,询问数据是否准备好。这会消耗大量 CPU。
-
I/O 多路复用 :
- 这是 select、poll、epoll 等系统调用的核心思想。
- 应用将一个或多个 I/O 请求(如多个网络连接)注册到一个"代理"上(如 epoll)。
- 然后这个"代理"会阻塞,等待任何一个被注册的 I/O 准备就绪,然后通知应用程序哪些 I/O 可以读了/写了。
- 这是构建高性能网络服务器的关键模型(如 Nginx、Redis),可以用单个线程管理成千上万的连接。
-
信号驱动 I/O :
- 应用发起一个 I/O 请求,并注册一个信号处理函数。请求发出后,线程继续执行。
- 当内核数据准备好时,会向应用发送一个 SIGIO 信号。
- 应用在信号处理函数中进行实际的 I/O 操作。
-
异步 I/O :
- 应用发起一个 I/O 请求后,立即返回,继续做其他事情。
- 内核会完成整个 I/O 操作(包括等待数据和将数据拷贝到用户空间)。
- 操作完成后,内核通过某种机制(如回调函数)通知应用。
- 与信号驱动 I/O 的区别在于:AIO 是内核完成所有工作后通知;信号驱动是内核通知你"可以开始工作了",你自己还得去拷贝数据。
总结
| I/O 类型 | 常见例子 | 特点 |
| 存储 I/O | 读写硬盘文件 | 持久化存储,速度慢(相对于内存) |
| 网络 I/O | 网页请求、数据库远程连接 | 高延迟,不稳定,是现代后端开发的重点 |
| 外设 I/O | 键盘、鼠标、显示器 | 与用户交互,实时性要求高 |
| 进程间 I/O | 管道、共享内存 | 进程间数据交换,速度较快 |
所以,I/O 是一个极其广泛的概念,涵盖了计算机与外界(包括用户、网络、其他设备)的所有数据交换。 理解不同类型的 I/O 及其背后的模型,对于开发高效、健壮的软件至关重要。
(C): 导致线程阻塞(睡眠)的多种场景
I/O 操作 是导致线程阻塞最常见和典型的场景之一,无论是磁盘 I/O 还是网络 I/O。 除了 I/O,线程阻塞(或进入非运行状态)的场景还有很多。我们可以从操作系统调度和线程生命周期的角度来系统地理解它们。
本质上,当一个线程因为某些原因无法继续执行时,操作系统就会剥夺它的 CPU 时间片,并将其置于一种"等待"或"阻塞"状态,直到某个条件被满足。
以下是导致线程阻塞的主要场景,可以归为几大类:
1. 同步原语 和 锁
这是多线程编程中最常见的阻塞场景,目的是为了协调对共享资源的访问,防止数据竞争。
-
获取锁失败
- Synchronized(Java) / Mutex(C++): 当一个线程试图获取一个已经被其他线程持有的互斥锁时,它会被阻塞,直到锁被释放。
- ReentrantLock : 与
synchronized类似,但更灵活。调用lock()方法时如果锁不可用,线程会阻塞。
-
等待条件成立
- Object.wait() / Condition.await() : 线程在持有锁的情况下,主动调用这些方法会释放锁 并进入等待状态,直到其他线程调用
notify()/notifyAll()或signal()/signalAll()。这是"等待-通知"机制的核心。
- Object.wait() / Condition.await() : 线程在持有锁的情况下,主动调用这些方法会释放锁 并进入等待状态,直到其他线程调用
-
计数器未就绪
- CountDownLatch : 线程调用
await()方法会被阻塞,直到计数器减到 0。 - CyclicBarrier: 一组线程必须全部到达屏障点才会继续执行。先到达的线程会被阻塞,直到最后一个线程到达。
- CountDownLatch : 线程调用
-
信号量不足
- Semaphore : 线程调用
acquire()方法时,如果许可证数量不足,线程会被阻塞,直到有其他线程释放许可证。
- Semaphore : 线程调用
2. I/O 操作
这是经典阻塞场景。
- 网络 I/O : 当线程执行
Socket.read()或Socket.write()时,如果对端数据尚未到达、网络缓冲区已满等,线程会阻塞。 - 磁盘 I/O: 当线程读写文件时,如果数据尚未从磁盘加载到内核缓冲区,线程会等待磁盘 IO 完成。虽然现代操作系统有大量的缓存和预读机制,但在高负载或大量随机读写的场景下,阻塞仍然明显。
3. 线程协作 与 生命周期管理
- Thread.join() : 一个线程等待另一个线程执行完毕。线程 A 执行
threadB.join(),那么线程 A 会阻塞,直到线程 B 终止。
4. 超时 与 定时
- Thread.sleep(millis) : 这是线程主动让出 CPU,进入计时等待状态。在指定的毫秒数内,线程不会参与 CPU 调度。
- Object.wait(timeout) / Condition.await(timeout): 带有超时参数的等待,避免了无限期等待。
5. 用户输入
- System.in.read(): 等待用户在控制台输入数据。在用户按下回车键之前,线程会一直阻塞。
总结与对比
为了更清晰地理解,我们可以将阻塞场景分为:
| 场景类别 | 具体例子 | 触发条件 | 唤醒条件 |
|---|---|---|---|
| 同步与锁 | synchronized, ReentrantLock.lock() |
请求的资源被占用 | 资源被释放 |
| 等待通知 | Object.wait(), Condition.await() |
主动等待某个条件 | 被其他线程通知 |
| 线程协作 | Thread.join() |
等待另一个线程结束 | 目标线程运行结束 |
| I/O 操作 | Socket.read(), File.read() |
数据未就绪 | 数据就绪 |
| 主动暂停 | Thread.sleep() |
代码主动调用 | 指定的时间过去 |
| 用户交互 | System.in.read() |
等待用户输入 | 用户输入并回车 |
如何避免阻塞?------ 异步编程
在现代高并发操作系统中,为了避免大量线程阻塞耗尽系统资源,广泛采用了异步非阻塞的编程模型。
- NIO (Non-blocking I/O) : 线程发起一个 I/O 操作后立即返回,不会阻塞。当数据就绪时,通过类似事件通知的机制(如 Selector)再来处理(比如: 在 Linux 中,NIO 的核心就是 I/O 多路复用 + 非阻塞 I/O ,而
epoll是目前最高效的实现。它让单个线程能够处理成千上万的网络连接,这正是现代高并发服务器(如 Nginx、Redis)能够支撑海量连接的技术基础。)。 - CompletableFuture (Java) / Promise (JavaScript): 代表一个异步计算的结果。你可以在其上附加回调函数,当计算完成时自动调用,而不需要阻塞等待。
- Async/Await (C#, Python, JavaScript 等): 一种让异步代码写起来像同步代码的语法糖,底层仍然是基于回调或事件循环,但极大地提高了代码的可读性。