这个问题其实是很多人刚学 Node 时最容易混淆的点:
JS 单线程 ≠ Node 只能同时处理一个请求
Node 能处理高并发,原因是:
diff
JS执行线程单线程
+
事件循环(Event Loop)
+
非阻塞I/O
+
操作系统异步能力
+
libuv线程池
共同实现的。
为什么会有误解?
假设有两个请求:
css
用户A 请求数据库
用户B 请求数据库
很多人以为:
css
JS单线程
A执行完
↓
B再执行
实际上不是。
看一个例子
dart
app.get('/user', async (req, res) => {
const data = await queryDB();
res.send(data);
});
当请求进来:
scss
请求A
↓
queryDB()
Node做的事情:
发起数据库请求
↓
立即返回
↓
继续接收其他请求
注意:
等待数据库结果
并没有占用JS线程
真实执行过程
时间点1
请求A:
scss
queryDB()
数据库需要:
500ms
Node:
把任务交出去
时间点2
数据库还没返回:
499ms
此时:
css
请求B来了
请求C来了
请求D来了
Node继续处理:
css
B
C
D
...
所以:
500ms等待期间
JS线程没有闲着
为什么能这样?
因为I/O是异步的。
例如:
scss
fs.readFile()
axios.get()
mysql.query()
redis.get()
这些本质都是:
css
I/O操作
执行流程:
JS线程
↓
libuv
↓
操作系统
↓
等待结果
↓
回调通知
JS线程不用傻等。
一个形象的例子
Java同步阻塞模型
服务员:
客人点餐
↓
站厨房门口等20分钟
↓
送餐
这20分钟:
不能服务其他客人
Node模型
服务员:
客人点餐
↓
通知厨房
↓
离开
马上去:
服务其他桌
厨房做好:
通知服务员
服务员回来送餐。
所以:
一个服务员
服务很多桌
这就是高并发。
Event Loop 起什么作用?
当数据库返回结果:
vbnet
数据库完成
↓
回调进入队列
↓
Event Loop发现任务
↓
执行回调
↓
返回响应
例如:
ini
queryDB().then(data => {
res.send(data);
});
这里:
then回调
就是 Event Loop 调度执行的。
libuv线程池又干什么?
很多异步任务实际上需要线程。
例如:
scss
fs.readFile()
crypto.pbkdf2()
zlib.gzip()
dns.lookup()
这些任务:
JS线程
↓
libuv线程池
↓
工作线程执行
↓
结果返回
默认:
4个线程
所以:
Node不是完全单线程
准确说:
JavaScript执行线程单线程
Node运行时是多线程
为什么特别适合高并发?
因为Web系统绝大多数时间:
等待数据库
等待Redis
等待HTTP接口
等待文件
比如:
CPU计算:5ms
等待数据库:495ms
总耗时:
500ms
其中:
shell
99%
都在等待
Node把这99%的等待时间利用起来处理其他请求。
因此:
1个线程
能管理几千甚至几万个连接
面试回答(推荐背下来)
JavaScript 本身是单线程的,但 Node.js 采用事件驱动和非阻塞 I/O 模型。请求中的数据库查询、网络请求、文件读写等耗时操作不会阻塞主线程,而是交给操作系统或 libuv 线程池处理。主线程继续处理其他请求,当异步任务完成后,通过 Event Loop 将回调放回执行队列。因此 Node.js 虽然 JavaScript 执行线程是单线程,但能够高效利用等待时间,在 I/O 密集型场景下实现高并发处理。
面试官如果继续追问:
"既然 Node 这么厉害,为什么 Java、Go 还这么流行?"
标准回答是:
Node 擅长 I/O 密集型 场景(API服务、Web服务、网关等),但对于视频编码、图像处理、大数据计算等 CPU密集型 场景,单线程 Event Loop 容易被阻塞,而 Java、Go 更适合利用多核 CPU 进行高并行计算。