一 背景
最近在做一个项目设计时遇到一个下面的问题。
如果在一个项目中,处理每个任务的时间都较长,如需要 15s 才能处理完一个任务。那么一个项目处理任务的 QPS 则是一个需要考虑的性能点。
开始我简单的以为只是需要开启更多的线程来进行多线程处理。因为 15s 这个时间如果不能压缩的话,提高 QPS 就只能扩大线程池的容量,但是后来发现并非如此。
这里涉及到 java 和 go 中线程和协程的原理。
二 总览对比
特性 | Java 线程 (Thread) | Go 协程 (Goroutine) |
创建成本 | 高(每个线程大约消耗 1MB 栈内存) | 低(初始栈仅几 KB,可动态增长) |
调度方式 | OS 线程,由操作系统调度(抢占式) | 用户态调度,由 Go runtime 管理 |
数量支持 | 数千个线程易于耗尽资源 | 可以轻松支持成千上万个 goroutine |
阻塞行为 | 阻塞会挂起整个 OS 线程 | 使用 channel 和非阻塞 IO,调度器会自动切换 |
创建方式 | new Thread().start() 或线程池 | go func() |
通信方式 | 多使用共享内存 + 锁 | 倾向于使用 channel,CSP 模型 |
三 底层原理
3.1 Java 线程底层原理
- 线程模型:
- 每个 Thread 对应一个 内核线程(Kernel Thread),由操作系统(Windows/Linux)调度。
- 多线程切换时需要 内核态/用户态切换(上下文切换),成本高。
- 内存结构:
- 每个线程分配独立的栈空间(默认 1MB)。
- 多线程间共享堆内存,因此要注意并发同步(synchronized, Lock, volatile 等)。
- 调度机制:
- 操作系统级的线程调度器负责分配 CPU。
- Java 层可使用线程池 (ExecutorService) 控制线程复用
3.2 Go 协程底层原理(以 Go 1.21 为例)
- 线程模型:Go 使用 M:N 调度模型:
- 协程(G): 这些是使用
go关键字启动的实际任务或可执行代码单元 。协程退出后,其 - 处理器(P): 这些是逻辑处理器,通常每个 CPU 核心创建一个(可通过
GOMAXPROCS配置) 。每个 P 都充当协调器,维护一个可运行协程的本地运行队列,并为 OS 线程(M)执行协程(G)提供必要的“权限和资源” 。 - 机器(M): 这些是执行 Go 用户代码、运行时代码或系统调用的原生操作系统线程 。M 是实际运行协程的“工作者”。
Go 采用 M:N 调度器,这是一个由 Go 运行时本身而非操作系统管理的高度复杂的用户空间调度器 。在此模型中,M 个协程被多路复用到 N 个操作系统线程上,其中 M 可以远大于 N。
G-P-M 模型由三个核心组件组成:
g 对象会返回到空闲池中以供重用。- 栈空间管理:
- 初始每个 goroutine 只有 ~2KB 栈空间。
- 栈可以 动态增长和缩小(不是固定分配),这是协程轻量的关键。
- 调度器机制(Go runtime):
- 非抢占式调度为主,定期或遇阻塞点(如 syscall、channel 操作)触发调度。
- 从 Go 1.14 起,支持 抢占式调度,防止长时间占用 CPU。
- 使用 Work Stealing 算法 实现高并发调度效率。
- 通信机制:
- 倾向于使用 channel 传递消息,避免共享内存。
- 遵循 “Do not communicate by sharing memory; share memory by communicating.”
3.3 本质区别
项 | Java Thread | Go Goroutine |
属于 | OS 层线程 | 用户层轻量线程 |
创建成本 | 高 | 极低 |
切换代价 | 高(内核切换) | 低(用户态调度) |
并发模型 | 基于线程 + 锁 | 基于协程 + channel |
调度控制 | 操作系统 | Go runtime 调度器 |
最大数量 | 数千个 | 数百万个 |
3.4 Demo 说明
java
public class Demo { public static void main(String[] args) { new Thread(() -> { System.out.println("Hello from Java thread"); }).start(); } }
go
package main import "fmt" func main() { go func() { fmt.Println("Hello from Go goroutine") }() }
四 总结
- 高并发任务:选择 Go 更优,因为其 goroutine 能轻松承载百万级并发,不会受到线程创建开销和上下文切换限制。
- 底层可控性和成熟生态:Java 在企业级应用中生态成熟,配套工具完备(如线程池、锁机制、监控工具)。
- 阻塞处理:Java 的阻塞可能导致线程浪费;Go 的协程遇阻塞点能立即切换,不会影响整体调度。
