什么是 IO 密集,什么是 CPU 密集?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 操作系统基础:面试官想知道你是不是真理解 "CPU 在算什么" 和 "进程在等什么" 的区别。很多人干了三五年,嘴里挂着 "IO 密集型任务",真让他解释却说不出个所以然。

  2. 实践意识:这题不只是在背概念——能不能把这两种任务类型和线程池配置、压测调优、资源利用率挂钩,才是真正想看的。光会答 "CPU 密集用 N+1" 是不够的,你得说清为什么。

  3. 性能优化的底层思维:进一步考察你能不能从 "一个任务到底卡在哪" 这个角度去分析系统瓶颈,而不是上来就堆机器、堆缓存。

核心答案

先一句话定调:

  • CPU 密集型(CPU-bound):任务的大部分时间花在 CPU 计算上,比如加密、压缩、排序、数值计算。瓶颈是 CPU。
  • IO 密集型(IO-bound):任务的大部分时间花在 等待 IO 上,比如读磁盘、查数据库、调远程接口。瓶颈是 IO,CPU 大部分时间闲着。

看张对比表,一眼就能分清:

维度 CPU 密集型 IO 密集型
典型场景 加解密、压缩解压、排序、科学计算、正则匹配 文件读写、数据库查询、HTTP 调用、消息队列消费
瓶颈在哪 CPU 算力 磁盘 / 网络 / 数据库 等外设速度
CPU 利用率 高(经常 100%) 低(大量时间阻塞等待)
线程状态 大部分时间 RUNNABLE 大部分时间 BLOCKED / WAITING
多线程收益 不大(CPU 就那么多,开多了反而切换开销) 显著(一个线程等 IO,CPU 还能跑别的线程)
推荐线程数 N + 1(N = CPU 核心数) 2N 或更高,甚至几十上百

记住一句话:"算得慢" 是 CPU 密集,"等得久" 是 IO 密集。

CPU 密集 IO 密集对比
CPU 密集 IO 密集对比

深度解析

一、为什么这两类任务这么不一样

这事得从操作系统层面说起。一个线程在跑的时候,要么 CPU 在干活(真正在执行指令),要么在等某个外部资源(IO 操作返回)。

线程运行等待状态
线程运行等待状态

上图把一个线程的运行切成两段:要么在 CPU 上跑,要么在等 IO。关键区别来了:

  • CPU 密集型任务:线程几乎一直待在 RUNNABLE 状态,CPU 被占得满满的。这时候你再开更多线程也没用,物理核数就那么几个,多出来的线程只能排队,还会带来上下文切换的开销,反而更慢。
  • IO 密集型任务:线程大部分时间在 BLOCKEDWAITING 状态,CPU 是闲着的。这时候多开几个线程,一个线程等 IO 的时候,另一个线程立刻把 CPU 用起来,整体吞吐量就上去了。

这就是为什么这两类任务对应的线程池配置完全不一样,背后是操作系统原理在撑着,不是拍脑袋定的。

二、推荐线程数怎么算

这块是面试官最爱追问的,也是工程上最实用的。

CPU 密集型:N + 1

N 是 CPU 核心数,比如 8 核机器就配 9 个线程。为什么不是 N?因为偶尔会有个线程因为缺页中断、或者突发 IO 挂起,多 1 个线程能把这段空闲填上,不至于 CPU 空转。再多就浪费了。

IO 密集型:经验公式是 2N

更严谨的公式其实是 N × (1 + WT/ST),其中:

  • WT(Wait Time)= 线程等待 IO 的时间
  • ST(Service Time)= 线程实际占用 CPU 的时间

举个例子:一个任务处理一次请求,CPU 真正算只要 50ms,但等数据库要 200ms,那 WT/ST = 4,单核机器理论上能配 1 × (1 + 4) = 5 个线程,8 核机器就是 40 个线程。

当然,公式算出来只是参考值,生产环境必须靠压测调。我之前压过一个 RPC 服务,公式算下来是 32 线程,实测 24 线程的吞吐反而最高——因为下游数据库扛不住更高的并发。

线程池线程数配置
线程池线程数配置

三、Java 里怎么判断我的任务是哪种

光会答概念不够,实际项目里怎么判断才是关键。给你三个实操手段:

// 方式一:看 arthas 的 thread 命令
// 直接观察线程状态分布,大部分是 RUNNABLE → CPU 密集
// 大部分是 BLOCKED/WAITING → IO 密集

// 方式二:jstack 连打几次,看线程在干嘛
jstack <pid> | grep -E "RUNNABLE|BLOCKED" | sort | uniq -c

// 方式三:看 CPU 利用率
// top -H 或用 Prometheus 监控
// CPU 一直高位 → CPU 密集
// CPU 上不去,但响应慢 → 大概率 IO 密集

更靠谱的办法是上 APM 工具(比如 SkyWalking、Pinpoint),直接看一次请求的耗时分布,多少花在 DB、多少花在 RPC、多少花在本地计算,一目了然。

四、容易踩的坑

几个新手常犯的错误,掉过一次就记住了:

  1. 以为所有 Web 服务都是 IO 密集型:大部分业务系统确实是(查数据库 + 调接口),但如果你做的是网关里的报文加解密、或者大数据的本地聚合计算,那就偏 CPU 密集,线程池不能照抄 2N。
  2. CPU 密集型任务开几十个线程:典型反面教材,线程多了上下文切换开销超过并行收益,CPU 利用率反而下降。这种系统一旦上线,运维看 vmstatcs(context switch)一栏会高得离谱。
  3. Executors.newCachedThreadPool() 处理高并发 IO:理论上它适合短生命周期的轻量任务,但请求量一上来,线程数可能瞬间飙到几千,直接把系统搞挂。还是得手动配 ThreadPoolExecutor

面试高频追问

  1. 追问一:那一个任务里既算又查库,算哪种?

    绝大多数业务任务都是混合型的——既有 CPU 计算,又有 IO 调用。这时候要看占比:如果 90% 时间在等 DB,那就按 IO 密集配;如果两者差不多,就要拆分任务,把 CPU 计算和 IO 操作分到不同的线程池里,互不影响。

混合型任务线程池拆分
混合型任务线程池拆分

  1. 追问二:线程数配多了或配少了分别什么后果?

    配少了,任务排队、响应变慢;配多了,上下文切换频繁,内存占用高(每个线程默认 1MB 栈空间),甚至 OOM。所以这是个先估算、后压测的过程。

  2. 追问三:协程 / 虚拟线程能解决 IO 密集型问题吗?

    能,而且解决得很漂亮。JDK 21 的虚拟线程(JEP 444)就是为 IO 密集型场景生的——一个 IO 操作挂起时,虚拟线程让出载体线程,几乎零成本切换。对 IO 密集型应用,虚拟线程能把并发量从平台线程的几百个,拉到几万甚至上百万级别,业务代码还不用改。不过有个坑要注意:synchronized 块里的 IO 操作会把虚拟线程 "钉" 在载体线程上,建议用 ReentrantLock 替代。

常见面试变体

  • "线程池的线程数怎么配?"
  • "为什么 CPU 密集型任务线程数不能开太多?"
  • "怎么判断你的应用是 CPU 密集还是 IO 密集?"
  • "虚拟线程解决了什么问题?"

记忆口诀

"算多用 N+1,等久开 2N;混合先拆分,压测定真值。"

  • 算(CPU 计算多)→ N + 1
  • 等(IO 等待久)→ 2N 起
  • 混合型 → 先拆分 CPU 和 IO 部分
  • 最后都以压测为准

总结

一句话:CPU 密集型卡在算力,线程数不能多,核心是榨干 CPU;IO 密集型卡在等待,线程数要够,核心是在等待间隙把 CPU 利用起来。配置线程池时,先判断任务类型,再用经验公式估算,最后压测微调,这才是工程上靠谱的做法。