Skip to content

Go 并发编程 ​

goroutine、channel 与同步原语,深入 GMP 调度模型,并与 Java 线程 / Kotlin 协程对照。


一、并发模型总览 ​

1.1 并发与并行 ​

  • 并发(concurrency):同时处理多件事的结构设计,是程序结构问题。
  • 并行(parallelism):同时执行多件事,是运行问题,依赖多核硬件。

Go 的设计理念是"你只管写出并发结构(goroutine + channel),调度器在可能时自动并行执行"。这句话翻译过来:Go 帮你做了 Java 中线程池 + Future + 回调的组合工作。

1.2 从 OS 线程到 goroutine ​

Java 中一个 new Thread(...).start() 背后是 1:1 的 OS 线程:内核态创建、默认栈 1MB、上下文切换要进内核。而 goroutine 是 Go 运行时自己管理的用户态协程:

维度Java 线程goroutine
归属OS / JVMGo runtime
初始栈约 1MB(固定)2KB(可动态扩缩到 1GB)
创建成本高(走内核)低(用户态,纳秒级)
切换成本内核上下文切换用户态,微秒级以下
规模上限数千到数万轻松百万级

go 关键字一条语句就启动一个 goroutine,无返回值、无 join 句柄(同步交给 channel / WaitGroup):

go
go func() {
    fmt.Println("in goroutine")
}()
go func() {
    fmt.Println("in goroutine")
}()

1.3 并发编程三原则(Go 官方格言) ​

  1. Do not communicate by sharing memory; instead, share memory by communicating.(不要通过共享内存来通信,而要通过通信来共享内存。)
  2. goroutine 不是免费的,但非常便宜,可大量使用。
  3. 不要启动你不知道何时结束的 goroutine(防泄漏)。

二、goroutine 深入 ​

2.1 栈管理:小栈起步,动态增长 ​

每个 goroutine 初始栈仅 2KB,按需增长(连续栈机制:扩容时分配 2 倍新栈并拷贝),用完收缩。百万 goroutine 的峰值内存约 2GB 仅是理论上限,实际远小于 Java 百万线程(1TB 栈)的荒谬程度。

2.2 goroutine 泄漏:最贵的 Bug ​

泄漏 = goroutine 永久阻塞,其栈与引用对象永不回收。三大来源与对策:

泄漏来源例子对策
channel 没人收ch <- x 永远阻塞用 select + ctx / timeout
忘记关闭通知消费者死等 range ch生产者 close 或改用 ctx
锁未释放 / 阻塞调用Mutex 忘记 Unlockdefer 解锁 + go vet 检查

排查手段:runtime.NumGoroutine() 打点监控;go tool pprof 抓 goroutine 堆栈;go test -run xxx 配合 goleak(Uber)在测试结尾断言无泄漏。

2.3 runtime 控制:Gosched 与 GOMAXPROCS ​

go
runtime.GOMAXPROCS(0) // 查询;设置默认不推荐,交给默认值(= 核数)
runtime.Gosched()     // 让出 P,一般不需要手动调用
runtime.NumGoroutine()// 当前 goroutine 数(监控用)
runtime.GOMAXPROCS(0) // 查询;设置默认不推荐,交给默认值(= 核数)
runtime.Gosched()     // 让出 P,一般不需要手动调用
runtime.NumGoroutine()// 当前 goroutine 数(监控用)

三、channel:通信即同步 ​

3.1 channel 基础 ​

channel 是带类型的管道,创建用 make(引用类型):

go
ch := make(chan int)   // 无缓冲
ch2 := make(chan int, 10) // 有缓冲,容量 10

ch <- 1    // 发送
v := <-ch  // 接收
v, ok := <-ch // 带 ok 判断通道是否已关闭
close(ch)  // 关闭
ch := make(chan int)   // 无缓冲
ch2 := make(chan int, 10) // 有缓冲,容量 10

ch <- 1    // 发送
v := <-ch  // 接收
v, ok := <-ch // 带 ok 判断通道是否已关闭
close(ch)  // 关闭

发送与接收都是阻塞语义,正好天然实现了同步(Java 的 SynchronousQueue 是它的带类型亲戚)。

3.2 有缓冲与无缓冲 ​

类型发送时机效果
无缓冲必须有接收者就绪发送者与接收者同时就绪才交接(同步)
有缓冲缓冲未满即可发发送者不阻塞,起到队列 / 限流作用

无缓冲 channel 常用来做两个 goroutine 间的握手信号,有缓冲 channel 则是简易队列。缓冲容量填满后继续发送会阻塞,这就是天然背压(backpressure)。

3.3 关闭与 range ​

发送方负责 close,接收方用 for range 优雅消费完剩余值:

go
func produce(ch chan<- int) { // chan<- int 单向:只写
    for i := 0; i < 5; i++ { ch <- i }
    close(ch) // 发送方关闭,之后接收方 range 自然结束
}

func main() {
    ch := make(chan int, 3)
    go produce(ch)
    for v := range ch { // 读到关闭为止,等价于每轮检查 ok
        fmt.Println(v)
    }
}
func produce(ch chan<- int) { // chan<- int 单向:只写
    for i := 0; i < 5; i++ { ch <- i }
    close(ch) // 发送方关闭,之后接收方 range 自然结束
}

func main() {
    ch := make(chan int, 3)
    go produce(ch)
    for v := range ch { // 读到关闭为止,等价于每轮检查 ok
        fmt.Println(v)
    }
}

规则:永远只在发送方关闭 channel;接收方不能也不应关闭。 关闭后接收会立即拿到零值,配合 v, ok := <-ch 区分。

3.4 单向 channel ​

chan<- T(只写)与 <-chan T(只读)用于函数签名,限制使用权限,编译期防呆。双向 channel 可隐式转为单向(反之不行),如上面示例的 produce(ch chan<- int)。

3.5 select:多路复用 ​

select 同时等待多个 channel,谁先就绪执行谁(随机公平选择),是 Go 并发的瑞士军刀:

go
select {
case v := <-jobs:
    process(v)
case <-time.After(3 * time.Second):
    fmt.Println("超时,放弃等待")
case <-ctx.Done():
    fmt.Println("被取消:", ctx.Err())
default: // 非阻塞分支,没有就绪的立即走这里
    fmt.Println("暂无任务")
}
select {
case v := <-jobs:
    process(v)
case <-time.After(3 * time.Second):
    fmt.Println("超时,放弃等待")
case <-ctx.Done():
    fmt.Println("被取消:", ctx.Err())
default: // 非阻塞分支,没有就绪的立即走这里
    fmt.Println("暂无任务")
}
  • 多个 case 同时就绪时 select 随机选一个(公平性)。
  • 没有 default 且无就绪 case 时会阻塞。
  • for { select {} } 是事件循环惯用结构。

3.6 channel 高频坑 ​

  1. 对已关闭 channel 发送会 panic:send on closed channel。
  2. 重复 close 会 panic:用 sync.Once 包裹 close。
  3. 关闭后接收立即返回零值:业务上必须用 ok 判断而非判断零值。
  4. nil channel 收发都会永久阻塞(可用于 select 中临时"禁用"某分支)。
  5. 忘记 close 不会 panic,但会让 range 死等——确认消费方语义再决定是否 close。

四、sync 包:显式同步原语 ​

4.1 WaitGroup:等待一组 goroutine ​

go
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done()
        fmt.Println(n)
    }(i)
}
wg.Wait() // 阻塞直到全部 Done
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done()
        fmt.Println(n)
    }(i)
}
wg.Wait() // 阻塞直到全部 Done
  • Add 必须在 Wait 之前(或并发执行 goroutine 前)调用完毕。
  • Done 等价于 Add(-1);计数器不能为负,否则 panic。
  • WaitGroup 不能复制(拷贝后计数器丢失),传递要用指针。

4.2 Mutex 与 RWMutex ​

go
type Counter struct {
    mu sync.Mutex
    n  int
}

func (c *Counter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock() // defer 保证解锁,配合 recover 兜底
    c.n++
}
type Counter struct {
    mu sync.Mutex
    n  int
}

func (c *Counter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock() // defer 保证解锁,配合 recover 兜底
    c.n++
}
  • sync.Mutex:互斥锁(Java synchronized / ReentrantLock)。不可重入:同一 goroutine 重复 Lock 会死锁。
  • sync.RWMutex:读写锁,多读单写(Java ReentrantReadWriteLock)。读锁可并发,写锁独占;写锁优先于读锁(防止写饥饿)。
  • 锁拷贝即灾难:Mutex 嵌入 struct 后,该 struct 只能以指针传递。

4.3 Once:只执行一次 ​

go
var (
    once sync.Once
    pool *sql.DB
)

func getDB() *sql.DB {
    once.Do(func() { pool = connect() }) // 并发下也仅执行一次
    return pool
}
var (
    once sync.Once
    pool *sql.DB
)

func getDB() *sql.DB {
    once.Do(func() { pool = connect() }) // 并发下也仅执行一次
    return pool
}

标准库内部(如 sync.Map 的初始化)大量使用 Once;它保证函数要么完整执行要么不执行,是"懒加载单例"的安全写法(Java 对应双重检查锁的繁琐)。

4.4 Cond:条件变量 ​

Cond 让 goroutine 等待"条件成立",比轮询省 CPU:

go
c := sync.NewCond(&sync.Mutex{})
ready := false

go func() {
    c.L.Lock()
    for !ready { c.Wait() } // Wait 内部:解锁 -> 挂起 -> 唤醒后重新加锁
    c.L.Unlock()
    fmt.Println("开始干活")
}()

// 主流程准备就绪
c.L.Lock()
ready = true
c.Broadcast() // 唤醒全部;Signal 只唤醒一个
c.L.Unlock()
c := sync.NewCond(&sync.Mutex{})
ready := false

go func() {
    c.L.Lock()
    for !ready { c.Wait() } // Wait 内部:解锁 -> 挂起 -> 唤醒后重新加锁
    c.L.Unlock()
    fmt.Println("开始干活")
}()

// 主流程准备就绪
c.L.Lock()
ready = true
c.Broadcast() // 唤醒全部;Signal 只唤醒一个
c.L.Unlock()

坑:条件判断必须用 for 而非 if(防止虚假唤醒 / 错过信号,与 Java await 规范一致);否则可能出现被唤醒后条件仍不成立。

4.5 Map:并发安全的 map ​

go
var m sync.Map
m.Store("key", 1)
v, ok := m.Load("key")
m.LoadOrStore("key", 2) // 不存在才写入(原子)
m.Range(func(k, v any) bool { // 遍历
    return true // false 停止
})
var m sync.Map
m.Store("key", 1)
v, ok := m.Load("key")
m.LoadOrStore("key", 2) // 不存在才写入(原子)
m.Range(func(k, v any) bool { // 遍历
    return true // false 停止
})

适用场景有限:读多写少的"写时复制型"缓存、只增不减的注册表。普通业务 map 请用 Mutex + 原生 map,性能与类型安全都更好(sync.Map 无泛型,Go 1.24 仍无内置泛型版本,社区用泛型封装)。

4.6 Pool:对象复用池 ​

go
var bufPool = sync.Pool{
    New: func() any { return make([]byte, 0, 1024) },
}

func handle() {
    b := bufPool.Get().([]byte) // 取(可能新建)
    b = append(b, "data"...)
    // 用完放回
    bufPool.Put(b[:0])
}
var bufPool = sync.Pool{
    New: func() any { return make([]byte, 0, 1024) },
}

func handle() {
    b := bufPool.Get().([]byte) // 取(可能新建)
    b = append(b, "data"...)
    // 用完放回
    bufPool.Put(b[:0])
}
  • 专为降低高频临时对象 GC 压力设计(典型:json 编解码缓冲区、bytes.Buffer)。
  • Pool 中的对象可能被 GC 随时回收,不能假设取出的对象一定在。
  • 放回前必须重置状态,否则脏数据复用。

4.7 atomic:无锁原子操作 ​

go
var n atomic.Int64 // Go 1.19+ 泛型风格原子类型
n.Add(1)
v := n.Load()
n.Store(10)
n.CompareAndSwap(10, 11) // CAS
var n atomic.Int64 // Go 1.19+ 泛型风格原子类型
n.Add(1)
v := n.Load()
n.Store(10)
n.CompareAndSwap(10, 11) // CAS

atomic 适用于:计数器、标志位、状态机等简单共享变量。与 Java AtomicInteger / volatile 对应,但没有 volatile 关键字——Go 里所有并发共享读写都必须用原子或锁或 channel,否则就是数据竞争。


五、GMP 调度模型 ​

5.1 G / M / P 是什么 ​

符号全称职责
GGoroutine一个待执行的用户协程(含栈、状态)
MMachine工作线程,真正执行 Go 代码的 OS 线程
PProcessor调度上下文,持有本地可运行 G 队列(容量 256)与执行资源

运行时维护一张全局就绪队列 + 每个 P 一个本地队列。规则:

  • 一个 M 要执行 G 必须先绑定一个 P(M:P = N:1 关系,P 数量 = GOMAXPROCS)。
  • 新建 G 优先进当前 P 的本地队列,本地满则放全局队列。
  • P 本地队列空了:先偷全局队列,再去其它 P 的本地队列偷一半(work stealing)。
  • M 阻塞(如 syscall):P 让给其它 M 继续跑,避免核空闲。

5.2 调度流程与抢占 ​

触发调度的事件:go 语句新建 G、GC、系统调用返回、锁竞争、channel 操作、以及抢占。Go 1.14 引入基于信号的异步抢占:sysmon 监控线程发现某 G 运行超过 10ms,就发送信号强制它让出,防止死循环卡死整个进程(Java 的线程调度由内核抢占,Go 是协作 + 信号抢占的混合)。

5.3 为什么百万 goroutine 可行 ​

  1. 栈极小(2KB 起步)且共享 M 而非独占线程。
  2. 切换在用户态完成,不陷入内核,不涉及 CPU 亲和性丢失。
  3. 网络 I/O 由 netpoller(epoll / IOCP)接管,G 阻塞在 I/O 时真正让出 M,唤醒后重新入队——这是"同步写法 + 异步性能"的根基。

5.4 GOMAXPROCS 与容器陷阱 ​

GOMAXPROCS 默认等于 runtime.NumCPU(),但Go 不会自动感知容器 CPU 配额(cgroup),在 4 核限制的容器里跑在 32 核宿主机上时,会创建 32 个 P,线程数与调度开销虚高,延迟劣化。

解法:接入 go get go.uber.org/automaxprocs 并在 main 里 _ = automaxprocs.Set(),或手动 GOMAXPROCS=4。K8s 场景务必处理。


六、channel 与锁的选型 ​

6.1 两种并发哲学 ​

  • CSP(channel):并发单元之间不共享状态,通过消息传递协作;适合"流水线 / 生产者-消费者 / 事件驱动"。
  • 共享内存 + 锁(Mutex):保护一段临界区;适合"多个 goroutine 更新同一结构"(计数器、缓存、余额)。

Go 格言"通过通信共享内存"是倾向性建议而非禁令——sync.Mutex 保护 map 是标准做法,硬用 channel 反而别扭。

6.2 选型决策表 ​

场景首选理由
任务分发 / 流水线channel天然队列 + 背压
结果汇聚(多路返回)channel / WaitGroup扇入扇出
保护 map / 结构体字段Mutex / RWMutex简单直接
只执行一次的初始化sync.Once语义精确
高并发计数器atomic无锁最快
通知多个等待者Cond / channel closeclose 广播是 Go 惯用法
跨 goroutine 取消context官方标准

6.3 性能认知 ​

channel 底层也是"锁 + 队列"实现(hchan 内部有 mutex),不是魔法。但 Go 编译器与运行时对其做了深度优化(无竞争时的快速路径)。经验结论:channel 性能足够业务使用;只有在纳秒级热路径才需要考虑锁粒度与 atomic。判断标准是读代码的清晰度优先,过早优化是万恶之源。


七、内存模型与数据竞争 ​

7.1 Go 内存模型:happens-before ​

Go 内存模型与 Java 类似,定义同步操作间的 happens-before 关系:

操作保证
goroutine 创建go f() 之前的语句 happens-before f 执行
channel 发送发送 happens-before 对应接收完成
channel 关闭close 之前发生的 happens-before 从 channel 读到零值返回
无缓冲 channel 接收接收完成 happens-before 对应发送返回
MutexUnlock happens-before 后续 Lock 成功
WaitGroupDone happens-before Wait 返回

没有这些关系保护的两个 goroutine 读写同一变量,就是数据竞争(data race),行为未定义。

7.2 数据竞争示例 ​

go
var count int

func main() {
    for i := 0; i < 1000; i++ {
        go func() { count++ }() // 多个 goroutine 并发读写 count
    }
    time.Sleep(time.Second)
    fmt.Println(count) // 结果不确定,可能不是 1000
}
var count int

func main() {
    for i := 0; i < 1000; i++ {
        go func() { count++ }() // 多个 goroutine 并发读写 count
    }
    time.Sleep(time.Second)
    fmt.Println(count) // 结果不确定,可能不是 1000
}

count++ 是"读-改-写"三步,交错执行会丢更新。go build 不会报错,运行时也不一定 panic,危害极大。

7.3 race 检测器:必开 ​

bash
go test -race ./...       # 测试时检测
go run -race main.go      # 运行检测(性能下降约 5-10 倍,仅开发用)
go test -race ./...       # 测试时检测
go run -race main.go      # 运行检测(性能下降约 5-10 倍,仅开发用)

race 检测器(基于 ThreadSanitizer)能定位"哪个 goroutine 在哪个文件第几行竞争",开发与 CI 中常开:

text
==================
WARNING: DATA RACE
Read at 0x... by goroutine 7:
  main.main.func1() main.go:9 +0x...
Previous write at 0x... by goroutine 5:
  main.main.func1() main.go:9
==================
==================
WARNING: DATA RACE
Read at 0x... by goroutine 7:
  main.main.func1() main.go:9 +0x...
Previous write at 0x... by goroutine 5:
  main.main.func1() main.go:9
==================

修复方案按序选择:channel 传递 > atomic > 锁 > 重新设计(消除共享)。

7.4 与 Java 内存模型对照 ​

Java 有 volatile、final 特殊语义与 JMM 的可见性规则,Go 没有 volatile——因为 Go 的并发同步手段更少且更统一:要么用 channel/锁/原子,要么就是 bug。Java 的 synchronized 可重入、volatile 只保证可见性,Go 的 Mutex 不可重入且总是"锁 = 全屏障"。整体上 Go 更简单粗暴,犯错空间更小。


八、常见并发模式 ​

8.1 Worker Pool:固定并发度消费任务 ​

go
func workerPool(jobs <-chan int, size int) {
    var wg sync.WaitGroup
    for i := 0; i < size; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := range jobs {
                fmt.Printf("worker %d 处理 %d\n", id, j)
            }
        }(i)
    }
    wg.Wait()
}

func main() {
    jobs := make(chan int, 100)
    go func() {
        defer close(jobs)
        for i := 0; i < 100; i++ { jobs <- i }
    }()
    workerPool(jobs, 10) // 固定 10 个并发消费者
}
func workerPool(jobs <-chan int, size int) {
    var wg sync.WaitGroup
    for i := 0; i < size; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := range jobs {
                fmt.Printf("worker %d 处理 %d\n", id, j)
            }
        }(i)
    }
    wg.Wait()
}

func main() {
    jobs := make(chan int, 100)
    go func() {
        defer close(jobs)
        for i := 0; i < 100; i++ { jobs <- i }
    }()
    workerPool(jobs, 10) // 固定 10 个并发消费者
}

要点:任务关闭后 range 结束,wg.Wait 保证全部处理完再返回。

8.2 扇出(Fan-out)与扇入(Fan-in) ​

go
// 扇出:一个源 -> 多个 worker;扇入:多个 worker 结果 -> 一个汇聚 channel
func merge(chs ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    wg.Add(len(chs))
    for _, c := range chs {
        go func(c <-chan int) {
            defer wg.Done()
            for v := range c { out <- v }
        }(c)
    }
    go func() { wg.Wait(); close(out) }() // 全部结束后关闭汇聚 channel
    return out
}
// 扇出:一个源 -> 多个 worker;扇入:多个 worker 结果 -> 一个汇聚 channel
func merge(chs ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    wg.Add(len(chs))
    for _, c := range chs {
        go func(c <-chan int) {
            defer wg.Done()
            for v := range c { out <- v }
        }(c)
    }
    go func() { wg.Wait(); close(out) }() // 全部结束后关闭汇聚 channel
    return out
}

这是"分治 + 汇总"的通用骨架,注意关闭时机:close(out) 必须在所有生产者退出后执行。

8.3 超时控制 ​

go
ch := make(chan result)
go func() { ch <- doSlow() }()

select {
case r := <-ch:
    fmt.Println("成功:", r)
case <-time.After(2 * time.Second):
    fmt.Println("超时")
}
ch := make(chan result)
go func() { ch <- doSlow() }()

select {
case r := <-ch:
    fmt.Println("成功:", r)
case <-time.After(2 * time.Second):
    fmt.Println("超时")
}

注意 time.After 每次 select 都会新建 timer,高频循环里应改用 time.NewTimer 并在退出分支 Stop,避免 timer 泄漏堆积。

8.4 context 取消:从根上停掉整棵树 ​

go
func run(ctx context.Context) error {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel() // 本函数返回时通知所有子任务停止

    results := make(chan int)
    go func() { // 子任务监听取消
        select {
        case results <- heavyWork():
        case <-ctx.Done():
            return
        }
    }()

    select {
    case v := <-results:
        fmt.Println(v)
    case <-ctx.Done():
        return ctx.Err() // context canceled
    }
    return nil
}
func run(ctx context.Context) error {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel() // 本函数返回时通知所有子任务停止

    results := make(chan int)
    go func() { // 子任务监听取消
        select {
        case results <- heavyWork():
        case <-ctx.Done():
            return
        }
    }()

    select {
    case v := <-results:
        fmt.Println(v)
    case <-ctx.Done():
        return ctx.Err() // context canceled
    }
    return nil
}

生产规范:所有 goroutine 的启动处都应思考"它如何退出",三条路——channel 关闭、ctx.Done、或自然结束。

8.5 信号量限流 ​

go
sem := make(chan struct{}, 5) // 缓冲 struct{} 即信号量

for _, task := range tasks {
    sem <- struct{}{} // 超过 5 个并发则阻塞等待
    go func(t Task) {
        defer func() { <-sem }()
        t.Run()
    }(task)
}
sem := make(chan struct{}, 5) // 缓冲 struct{} 即信号量

for _, task := range tasks {
    sem <- struct{}{} // 超过 5 个并发则阻塞等待
    go func(t Task) {
        defer func() { <-sem }()
        t.Run()
    }(task)
}

struct{} 零字节,是"纯信号"channel 的标准写法。也可用 golang.org/x/sync/semaphore 获得权重信号量。

8.6 重试与退避 ​

go
func retry(attempts int, fn func() error) error {
    for i := 0; i < attempts; i++ {
        if err := fn(); err == nil {
            return nil
        } else if i == attempts-1 {
            return err
        }
        time.Sleep(time.Duration(1<<i) * 100 * time.Millisecond) // 指数退避 100ms,200ms,400ms
    }
    return nil
}
func retry(attempts int, fn func() error) error {
    for i := 0; i < attempts; i++ {
        if err := fn(); err == nil {
            return nil
        } else if i == attempts-1 {
            return err
        }
        time.Sleep(time.Duration(1<<i) * 100 * time.Millisecond) // 指数退避 100ms,200ms,400ms
    }
    return nil
}

生产建议配合 jitter(随机抖动)避免惊群,可直接用 github.com/cenkalti/backoff。


九、与 Java / Kotlin 并发模型对比 ​

9.1 三者的"轻量并发"方案 ​

维度goroutineJava 虚拟线程(Loom)Kotlin 协程
语言Go 原生JDK 21+库(kotlinx.coroutines)
栈2KB 动态可挂起堆栈状态机(无独立栈)
调度运行时 M:NJVM 调度线程池 + 调度器
通信channel / 锁锁 / 并发集合channel / Flow / Mutex
取消context中断(不可靠)结构化并发 + Job
学习曲线平缓(一条语言特性)需要理解 JMM需要理解挂起机制

三者本质都是"把阻塞换成交出控制权",让线程空转时间变成有效吞吐。Java 虚拟线程对标 goroutine 最直接;Kotlin 协程通过挂起函数把异步写得像同步,比 Go 的显式 goroutine 更隐蔽但也更容易失控。

9.2 同步原语对照 ​

GoJava说明
sync.Mutexsynchronized / ReentrantLockGo 不可重入
sync.RWMutexReentrantReadWriteLock多读单写
sync.WaitGroupCountDownLatch / CyclicBarrier等待一组完成
sync.Once双重检查锁只执行一次
sync.CondCondition条件等待
channelBlockingQueue / SynchronousQueue消息传递
atomicAtomicLong / volatile原子操作
sync.MapConcurrentHashMap并发容器(场景窄)
contextCompletableFuture 取消 / 线程中断取消传播
goroutine + channelExecutorService + Future任务编排

9.3 设计哲学总结 ​

  • Java:以锁 + 并发集合为中心,强调线程安全容器;并发工具从 JUC 选择组合。
  • Go:以 goroutine + channel 为中心,鼓励"每个独立任务一个 goroutine,用管道串联";锁只兜底临界区。
  • Kotlin:结构化并发,suspend 函数链式表达异步流程,最接近命令式业务代码。

没有银弹:Go 的写法在大流量网关、代理类服务中开发效率极高;Java/Kotlin 在复杂业务状态机与既有 Spring 生态里更顺手。转换心态的钥匙是——别把一个 goroutine 当线程用,把它当"廉价的独立任务"用。


本篇已覆盖 Go 并发全貌。下一步落地到 Web 开发:Go Gin 框架 与 Go Web 项目实践。