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
    8
    class MyThread extends Thread {
    @Override
    public void run() {
    System.out.println("Thread running");
    }
    }
    // 使用
    new MyThread().start();

    实现 Runnable 接口(更推荐):避免继承的局限性,更灵活。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class MyRunnable implements Runnable {
    @Override
    public void run() {
    System.out.println("Runnable running");
    }
    }
    // 使用
    Thread t = new Thread(new MyRunnable());
    t.start();

    实现 Callable 接口:可以返回结果或抛出异常,通常与FutureTask和线程池结合使用。

    1
    2
    3
    4
    5
    6
    7
    Callable<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
    9
    func 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
    14
    func 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
    11
    private 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
    10
    func main() {
    ch := make(chan int) // 创建一个传递 int 的无缓冲 channel

    go func() {
    ch <- 42 // 向 channel 发送数据
    }()

    value := <-ch // 从 channel 接收数据
    fmt.Println(value) // 输出: 42
    }
    • 无缓冲 Channel:发送和接收会阻塞,直到另一方准备好,是强同步的。
    • 有缓冲 Channelmake(chan int, size),只有在缓冲区满或空时才会阻塞。
  • Select 语句:用于同时处理多个 Channel,类似于 switch 但专为 Channel 设计。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    select {
    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
    8
    var 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 要确保能正常结束,防止内存泄露。
  • Goroutine 泄漏

    :如果 Goroutine 启动后永远阻塞(如在一个再无人发送的 Channel 上等待),会导致泄漏。使用 context.Context 来实现取消和超时机制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ctx, 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
    22
    func 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 网关、微服务、分布式系统