Go和Java多线程
Java 与 Go 并发编程对比
1 并发模型核心理念
Java 和 Go 在并发编程上采用了截然不同的哲学和实现模型。
Java 采用共享内存模型。其并发基于操作系统线程(内核线程),你编写的每个 Thread 都直接对应一个操作系统线程。多个线程共享进程内存空间,这使得数据共享直接,但也带来了复杂的同步问题。线程间通信主要通过读写共享变量,并使用锁(如 synchronized)或其他同步机制(如 wait()/notify())来协调访问、避免数据竞争,Java 的线程是抢占式的,由操作系统内核进行调度,这可能导致调度开销较大,尤其是在线程数量很多时。
Go 则推崇 “通过通信来共享内存” 而非“通过共享内存来通信”。其核心是 goroutine,这是一种由 Go 运行时管理的轻量级线程(协程)。大量 goroutine 可以在少量操作系统线程上高效复用。Go 运行时调度器采用 GPM 模型(Goroutine, Processor, Machine),在用户态进行协作式调度,调度开销极低。goroutine 间的通信首选**通道 (channel)**,它是一种类型安全的队列,用于在不同的 goroutine 之间安全地传递数据和同步执行。虽然 Go 也提供了传统的同步原语(如 sync.Mutex),但更鼓励使用 channel 来解耦并发操作。
| 特性 | Java | Go |
|---|---|---|
| 并发单元 | 线程 (Thread), 重量级 | 协程 (Goroutine), 轻量级 |
| 创建方式 | 继承 Thread 类或实现 Runnable/Callable 接口 |
go 关键字 |
| 调度机制 | 操作系统内核抢占式调度 | Go 运行时协作式调度 (GPM 模型) |
| 内存开销 | 较大 (默认约 1MB/线程) | 极小 (初始约 2KB/goroutine) |
| 通信机制 | 主要通过共享内存,使用锁同步 (synchronized, Lock) |
主要通过通道 (Channel) |
| 设计哲学 | 通过共享内存进行通信 | 通过通信来共享内存 |
| 优势场景 | CPU 密集型任务、复杂的企业级应用 | I/O 密集型任务、高并发网络服务 |
2 并发单元的创建与管理
2.1 Java 线程创建与管理
Java 中线程是相对重量级的资源,直接由操作系统调度。
创建方式:
继承
Thread类,重写 run()方法1
2
3
4
5
6
7
8class MyThread extends Thread {
public void run() {
System.out.println("Thread running");
}
}
// 使用
new MyThread().start();实现
Runnable接口(更推荐):避免继承的局限性,更灵活。1
2
3
4
5
6
7
8
9class MyRunnable implements Runnable {
public void run() {
System.out.println("Runnable running");
}
}
// 使用
Thread t = new Thread(new MyRunnable());
t.start();实现
Callable接口:可以返回结果或抛出异常,通常与FutureTask和线程池结合使用。1
2
3
4
5
6
7Callable<String> task = () -> {
Thread.sleep(1000);
return "Task Done";
};
FutureTask<String> future = new FutureTask<>(task);
new Thread(future).start();
System.out.println(future.get()); // 获取返回值**线程池 (ThreadPool)**:为减少频繁创建和销毁线程的开销,Java 提供了强大的线程池支持 (
java.util.concurrent.ExecutorService)1
2
3
4
5
6
7
8// 示例:创建固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
System.out.println("Task executed by " + Thread.currentThread().getName());
});
}
executor.shutdown();核心参数:
corePoolSize: 核心线程数。maximumPoolSize: 最大线程数。workQueue: 任务队列(如ArrayBlockingQueue,LinkedBlockingQueue)。RejectedExecutionHandler: 拒绝策略(如AbortPolicy抛出异常)。
2.2 Go Goroutine 创建与管理
Goroutine 是 Go 的轻量级并发单元,由 Go 运行时调度,开销极小。
创建方式:使用
go关键字后跟函数调用。1
2
3
4
5
6
7
8
9func sayHello() {
fmt.Println("Hello from Goroutine!")
}
func main() {
go sayHello() // 启动一个 goroutine
fmt.Println("Hello from Main!")
time.Sleep(time.Second) // 防止主 goroutine 退出太快
}等待 Goroutine 完成:常用
sync.WaitGroup来同步。1
2
3
4
5
6
7
8
9
10
11
12
13
14func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // 增加计数器
go func(id int) {
defer wg.Done() // 完成后计数器减1
fmt.Printf("Goroutine %d done\n", id)
}(i)
}
wg.Wait() // 阻塞直到所有 goroutine 完成
fmt.Println("All goroutines finished.")
}
3 通信与同步机制
3.1 Java 的共享内存与锁
Java 线程间通信主要通过共享内存,因此同步是关键。
synchronized 关键字:用于修饰方法或代码块,确保同一时间只有一个线程可以执行。
1
2
3
4
5
6
7
8
9
10
11private int counter = 0;
public synchronized void increment() {
counter++; // 同步方法
}
public void doSomething() {
synchronized(this) { // 同步代码块
// ...
}
}
3.2 Go 的 Channel 与 Select
Go 推荐通过 Channel 在 Goroutine 间进行通信和同步。
**Channel (通道)**:是一种类型化的管道,用于在不同的 Goroutine 之间传递数据。
1
2
3
4
5
6
7
8
9
10func main() {
ch := make(chan int) // 创建一个传递 int 的无缓冲 channel
go func() {
ch <- 42 // 向 channel 发送数据
}()
value := <-ch // 从 channel 接收数据
fmt.Println(value) // 输出: 42
}- 无缓冲 Channel:发送和接收会阻塞,直到另一方准备好,是强同步的。
- 有缓冲 Channel:
make(chan int, size),只有在缓冲区满或空时才会阻塞。
Select 语句:用于同时处理多个 Channel,类似于
switch但专为 Channel 设计。1
2
3
4
5
6
7
8
9
10select {
case msg1 := <-ch1:
fmt.Println("Received", msg1)
case msg2 := <-ch2:
fmt.Println("Received", msg2)
case ch3 <- 3:
fmt.Println("Sent 3")
default:
fmt.Println("No communication")
}传统的同步原语:Go 也在
sync包中提供了Mutex,RWMutex,Once等,用于更复杂的场景或性能关键处。1
2
3
4
5
6
7
8var counter int
var mu sync.Mutex
func increment() {
mu.Lock()
defer mu.Unlock() // 使用 defer 确保解锁
counter++
}
4 使用习惯与注意事项
4.1 Java 并发编程注意事项
- 线程安全:始终注意对共享资源的访问。优先使用线程安全集合(如
ConcurrentHashMap)。 - 避免死锁:确保锁的获取顺序一致,考虑使用定时锁尝试(
tryLock)。 - 性能考量
- 线程创建和上下文切换开销大,务必使用线程池。
- 锁竞争会严重降低性能,尽量减少临界区范围。
- 复杂性问题:多线程代码难以编写、调试和维护。充分利用
java.util.concurrent包中的高级工具(如CountDownLatch,CyclicBarrier,Semaphore)。
4.2 Go 并发编程注意事项
Channel 使用原则:
- 关闭 Channel:由发送方关闭 Channel,接收方可通过
val, ok := <-ch判断是否关闭。 - 避免泄露:不使用的 Goroutine 要确保能正常结束,防止内存泄露。
- 关闭 Channel:由发送方关闭 Channel,接收方可通过
Goroutine 泄漏
:如果 Goroutine 启动后永远阻塞(如在一个再无人发送的 Channel 上等待),会导致泄漏。使用 context.Context 来实现取消和超时机制
1
2
3
4
5
6
7
8
9ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case <-time.After(5 * time.Second): //5秒超时
fmt.Println("Work done")
case <-ctx.Done():
fmt.Println("Cancelled or timeout:", ctx.Err()) //两秒超时
}context和After区别:
context是从开始创建的时候开始计时的,After是从调用的那一刻开始计时的
context可以手动cancel()取消
context父取消,子也会取消
父子context:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22func main() {
// 父 Context:3秒超时
parentCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// 子 Context:5秒超时(实际受父 Context 约束,最多3秒)
childCtx := context.WithTimeout(parentCtx, 5*time.Second)
// 启动子任务
go func(ctx context.Context) {
select {
case <-ctx.Done():
fmt.Println("子任务被取消:", ctx.Err()) // 实际由父 Context 触发
case <-time.After(4 * time.Second):
fmt.Println("子任务完成") // 不会执行!
}
}(childCtx)
// 等待父 Context 超时
<-parentCtx.Done()
fmt.Println("父任务超时")
}场景 示例 微服务调用链 A 服务调用 B 服务时,传递父 Context 确保统一超时和取消。 HTTP 请求嵌套 主请求设置超时,子请求(如数据库查询)继承该超时。 跨 Goroutine 任务 主任务取消时,自动终止所有关联的子任务。 链路追踪 在父 Context 中存储 TraceID,子 Context 自动继承。 循环变量捕获:在循环中启动 Goroutine 时,务必将循环变量作为参数传入,否则所有 Goroutine 可能会共享同一个(最终)变量值。
1
2
3
4
5
6
7
8
9
10
11
12
13// 错误示范
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // 很可能全部打印5
}()
}
// 正确做法
for i := 0; i < 5; i++ {
go func(id int) {
fmt.Println(id)
}(i) // 将 i 作为参数传入
}不要过度使用 Goroutine:虽然轻量,但并非无限。控制并发数量(例如使用带缓冲的 Channel 作为信号量或 worker pool)。
5 性能特点与适用场景
| 方面 | Java | Go |
|---|---|---|
| 启动速度 | 较慢(需 JVM 启动、类加载) | 极快(静态编译,直接运行) |
| 内存占用 | 较高(线程默认栈大小 ~1MB) | 极低(Goroutine 初始栈 ~2KB) |
| 高并发表现 | 线程数多时,创建、切换开销大,性能下降 | 优势明显,可轻松创建数十万 Goroutine |
| GC 影响 | GC 技术成熟(G1, ZGC),但 Full GC 可能引发 STW | GC 优化良好(并发 GC),STW 时间短 |
| 典型应用场景 | CPU 密集型计算、复杂业务逻辑的大型企业应用 | 高并发网络服务、API 网关、微服务、分布式系统 |
