Mutex与WaitGroup
2025/12/15大约 4 分钟
本篇是你 Go 并发学习的 第 12 天:sync 包中的 Mutex/RWMutex、WaitGroup,重点对比 Java 的 synchronized、ReentrantLock、ReadWriteLock、CountDownLatch 等概念。
- 你的背景:Java 程序员
- 操作系统:Linux Mint XFCE
- Go 版本:go1.22.2 linux/amd64
- 项目目录示例:
/home/liumangmang/GolandProjects/go-sync-practice
📌 标题
Go 并发基础:sync 包 Mutex/RWMutex 与 WaitGroup 实战(对标 Java 锁与 CountDownLatch)
✅ 步骤 1:创建练习项目
cd /home/liumangmang/GolandProjects
mkdir go-sync-practice && cd go-sync-practice
go mod init go-sync-practice✅ 步骤 2:不加锁的共享变量问题(数据竞争)
先来看一个错误的写法,感受一下“数据竞争”(race condition):
创建 race_counter.go:
nano race_counter.go粘贴:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
counter := 0
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
counter++
}
}()
}
wg.Wait()
fmt.Println("期待的结果:", 1000*1000)
fmt.Println("实际结果:", counter)
}运行多几次:
go run race_counter.go你会发现 实际结果几乎总是小于 1_000_000,说明出现了数据竞争。
Java 类比:这就像在多个线程里对一个
int直接counter++,而没有用synchronized或AtomicInteger一样。
✅ 步骤 3:使用 sync.Mutex 保护共享数据
Go 的 sync.Mutex 就像 Java 的 ReentrantLock 或 synchronized,用于互斥访问某段临界区。
创建 mutex_counter.go:
nano mutex_counter.go粘贴:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var mu sync.Mutex
counter := 0
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
mu.Lock()
counter++
mu.Unlock()
}
}()
}
wg.Wait()
fmt.Println("期待的结果:", 1000*1000)
fmt.Println("实际结果:", counter)
}再运行几次:
go run mutex_counter.go你会看到结果稳定为 1_000_000。
心法:只要有“多个 goroutine 改同一份数据”的情况,就要考虑加锁或者使用 channel / atomic。
✅ 步骤 4:sync.RWMutex —— 读多写少场景的优化
Java 类比:
- 类似
ReentrantReadWriteLock:- 多个读可以并发;
- 写是独占的。
创建 rwmutex_cache.go:
nano rwmutex_cache.go粘贴:
package main
import (
"fmt"
"sync"
"time"
)
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func main() {
c := &Cache{data: make(map[string]string)}
c.Set("foo", "bar")
var wg sync.WaitGroup
// 多个读 goroutine
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 5; j++ {
v := c.Get("foo")
fmt.Printf("reader-%d 第 %d 次读到: %s\n", id, j, v)
time.Sleep(100 * time.Millisecond)
}
}(i)
}
// 一个写 goroutine
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(300 * time.Millisecond)
fmt.Println("writer: 更新 foo -> baz")
c.Set("foo", "baz")
}()
wg.Wait()
}运行:
go run rwmutex_cache.go你会看到在写之前,读到的都是 bar,写之后读到 baz;多个 reader 可以并行,写时会短暂阻塞读。
✅ 步骤 5:sync.WaitGroup —— 对标 Java 的 CountDownLatch
第 8 天你已经见过 WaitGroup,这里从 Java 角度再强化一下:
- Java CountDownLatch:
CountDownLatch latch = new CountDownLatch(N);- 每个任务
latch.countDown(); - 主线程
latch.await()。
- Go sync.WaitGroup:
wg.Add(N);- 每个 goroutine
defer wg.Done(); - 主 goroutine
wg.Wait()。
创建 waitgroup_like_latch.go:
nano waitgroup_like_latch.go粘贴:
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("worker-%d 开始工作\n", id)
time.Sleep(time.Duration(id) * 300 * time.Millisecond)
fmt.Printf("worker-%d 完成\n", id)
}
func main() {
var wg sync.WaitGroup
n := 3
wg.Add(n)
for i := 1; i <= n; i++ {
go worker(i, &wg)
}
fmt.Println("main: 等待所有 worker 完成...")
wg.Wait()
fmt.Println("main: 全部完成")
}运行:
go run waitgroup_like_latch.go注意事项:
Add一般在启动 goroutine 之前调用,避免“刚启动 goroutine 就已经 Done 完了”的竞态。WaitGroup只负责等待 数量归零,不区分成功/失败;如果需要结果,要自己用 channel 或别的结构传递。
✅ 步骤 6:锁 vs channel:什么时候用哪个?
给你一个简单的判断思路:
- 偏向用锁(Mutex/RWMutex)的场景:
- 多 goroutine 操作一份 嵌套数据结构(map、树、对象图)。
- 逻辑已经很面向“共享内存 + 加锁”思维,迁移成本低。
- 偏向用 channel 的场景:
- 更像“任务队列”“消息传递”:一个 goroutine 生产数据,另一个消费。
- 更容易建模为“流水线 / 队列 / 事件流”。
心法:Go 官方更提倡“不要通过共享内存来通信,而是通过通信来共享内存”。但作为 Java 背景,短期内完全用锁也没问题,慢慢再向 channel 思维过渡。
📚 今日小结与练习
你已经掌握:
- 不加锁的共享变量会产生数据竞争(race condition)。
- 用
sync.Mutex保护临界区,保证加减的原子性。 - 用
sync.RWMutex在读多写少时提升并发度。 - 用
sync.WaitGroup类比 JavaCountDownLatch等待一批任务完成。
练习建议:
- 在
race_counter.go上运行go run -race,体验 Go 自带的 data race 检测器(需要安装完整 Go 工具链)。 - 改写缓存例子,加入
LoadOrStore风格的逻辑:若 key 不存在则写入初始值。 - 将一个你熟悉的 Java 多线程 demo(比如多线程累加)改写成 Go 版本,分别用 Mutex 和 channel 实现一次,对比代码风格。
- 在
