RWMutex API详解 #
基本结构 #
type RWMutex struct {
// 内部实现,不需要关心
}
核心方法 #
| 方法 | 作用 | 使用场景 |
|---|---|---|
Lock() | 获取写锁 | 修改数据时 |
Unlock() | 释放写锁 | 修改完成后 |
RLock() | 获取读锁 | 读取数据时 |
RUnlock() | 释放读锁 | 读取完成后 |
TryLock() | 尝试获取写锁 | 非阻塞,Go 1.18+ |
TryRLock() | 尝试获取读锁 | 非阻塞,Go 1.18+ |
使用规则 #
var mu sync.RWMutex
var data map[string]int
// 写操作:独占
func write(key string, val int) {
mu.Lock()
defer mu.Unlock()
data[key] = val
}
// 读操作:可并发
func read(key string) int {
mu.RLock()
defer mu.RUnlock()
return data[key]
}
// 尝试锁(Go 1.18+)
func tryWrite(key string, val int) bool {
if mu.TryLock() {
defer mu.Unlock()
data[key] = val
return true
}
return false // 锁被占用
}
锁的规则矩阵 #
| 当前状态 | Lock() | RLock() |
|---|---|---|
| 无锁 | ✅ 成功 | ✅ 成功 |
| 有读锁 | ❌ 等待 | ✅ 成功 |
| 有写锁 | ❌ 等待 | ❌ 等待 |
RWMutex 基础概念 #
sync.RWMutex 是读写互斥锁,解决"多读单写"场景的并发问题:
- 多个 goroutine 可以同时读(通过
RLock()) - 写操作独占(通过
Lock()),写时不能有任何读或写
用微信群抢红包理解双重检查 #
场景设置 #
var (
红包 *Money = nil
红包锁 sync.RWMutex
被抢了 bool = false
)
错误做法:不做第二次检查 #
func 抢红包() {
// 第一次看:红包还在不在
红包锁.RLock()
if 被抢了 {
defer 红包锁.RUnlock()
return // 已经被抢了
}
红包锁.RUnlock()
// 红包还在!赶紧抢
红包锁.Lock()
defer 红包锁.Unlock()
// 错误:直接抢,不再检查
我的钱包 += 红包.金额 // 可能重复领取!
被抢了 = true
}
灾难场景:
老板发了100块红包,只有1个:
0.001秒:张三、李四、王五都看到红包还在
0.002秒:张三抢到了100块
0.003秒:李四也"抢到"了100块(重复领取!)
0.004秒:王五也"抢到"了100块(又重复!)
老板亏了200块!
正确做法:双重检查 #
func 抢红包() {
// 第一次检查
红包锁.RLock()
if 被抢了 {
defer 红包锁.RUnlock()
return
}
红包锁.RUnlock()
// 准备抢
红包锁.Lock()
defer 红包锁.Unlock()
// 第二次检查:再看一眼!
if !被抢了 { // 也许刚才等锁的时候已经被抢了
我的钱包 += 红包.金额
被抢了 = true
}
}
Lock的等待机制 #
多人同时Lock会怎样? #
当多个goroutine同时执行Lock()时:
时间线:
0.001秒:三个人都检查完,都解了读锁
0.002秒:三个人同时执行 Lock()
系统的处理:
- 张三:Lock() → 成功!(系统选了他)
- 李四:Lock() → 阻塞!等张三解锁
- 王五:Lock() → 阻塞!等张三解锁
0.003秒:张三抢完红包,Unlock()
0.004秒:系统选择李四获得锁
- 李四:获得锁 → 检查 → 被抢了 → 继续执行
- 王五:继续等待...
Lock的核心规则 #
- 只有一个人能成功,其他人排队等待
- 等待的goroutine会阻塞(睡眠),不消耗CPU
- 锁释放时,系统会唤醒一个等待者
红包锁.Lock() // 相当于:
// if 锁空闲 {
// 我拿到锁,继续执行
// } else {
// 加入等待队列,睡眠...
// 等被唤醒时继续
// }
为什么需要第二次检查? #
时间差问题 #
张三:[检查:有红包]→[解锁]→[等待获取写锁]→[拿到锁]
↑ ↓
这段时间是"空窗期" 需要再检查
李四可能已经抢了
不能锁中锁 #
// ❌ 错误:死锁
mutex.Lock()
mutex.Lock() // 自己等自己
// ❌ 错误:读锁中加写锁也会死锁
mutex.RLock()
mutex.Lock() // 写锁要等读锁释放,但读锁是自己的
// ✅ 正确:解锁后重新加锁
mutex.RLock()
// 检查
mutex.RUnlock() // 先释放
mutex.Lock() // 重新获取
// 再检查一次(关键!)
mutex.Unlock()
实际应用场景 #
Repository管理器示例 #
func GetBillRepository() BillRepository {
// 快速路径:99.9%的请求走这里
repoMux.RLock()
if initialized {
defer repoMux.RUnlock()
return globalBillRepo // 直接返回
}
repoMux.RUnlock()
// 慢速路径:只有初始化时走这里
repoMux.Lock()
defer repoMux.Unlock()
// 第二次检查
if !initialized {
globalBillRepo = NewBillRepository(db)
initialized = true
}
return globalBillRepo
}
性能对比 #
| 方案 | 特点 | 问题 |
|---|---|---|
| 普通Mutex | 每次都串行 | 性能差 |
| 不加锁 | 并发访问 | 数据竞争 |
| RWMutex+双重检查 | 读并发,写安全 | 最优 |
核心要点 #
- RWMutex适用场景:读多写少
- 双重检查目的:防止重复初始化
- Lock等待机制:阻塞队列,不消耗CPU
- 解锁再加锁:避免死锁,但需要二次检查
常见应用 #
- 单例模式
- 连接池初始化
- 配置文件加载
- 缓存系统
- 秒杀系统(防超卖)
常见错误 #
1. 忘记解锁 #
mu.Lock()
doSomething()
// 忘记 Unlock() - 死锁!
2. defer顺序错误 #
// 错误
mu.RLock()
if condition {
return // 没解锁就返回了!
}
defer mu.RUnlock()
// 正确
mu.RLock()
defer mu.RUnlock() // defer要紧跟锁
if condition {
return
}
3. 锁的粒度太大 #
// 不好
mu.Lock()
defer mu.Unlock()
doA() // 不需要锁
doB() // 需要锁
doC() // 不需要锁
// 好
doA()
mu.Lock()
doB()
mu.Unlock()
doC()
记住:不做第二次检查 = 一个红包被领三次
Go中的锁类型全览 #
1. sync.Mutex(互斥锁) #
var mu sync.Mutex
mu.Lock()
// 临界区:同时只有一个goroutine能执行
mu.Unlock()
使用场景:简单的互斥访问,保护共享资源 特点:最基础的锁,一次只允许一个goroutine访问
2. sync.RWMutex(读写锁) #
var rw sync.RWMutex
// 读操作:可多个并发
rw.RLock()
读取数据...
rw.RUnlock()
// 写操作:独占
rw.Lock()
修改数据...
rw.Unlock()
使用场景:读多写少的场景 特点:读锁可并发,写锁独占
3. sync.Once(一次性执行) #
var once sync.Once
var config *Config
func GetConfig() *Config {
once.Do(func() {
config = loadConfig() // 只执行一次
})
return config
}
使用场景:单例初始化、一次性配置加载 特点:保证函数只执行一次,内部有锁机制
4. sync.WaitGroup(等待组) #
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
处理任务(id)
}()
}
wg.Wait() // 等待所有任务完成
使用场景:等待多个goroutine完成 特点:不是锁,但用于并发协调
5. sync.Cond(条件变量) #
var mu sync.Mutex
cond := sync.NewCond(&mu)
queue := []string{}
// 消费者
func consumer() {
cond.L.Lock()
for len(queue) == 0 {
cond.Wait() // 等待条件满足
}
item := queue[0]
queue = queue[1:]
cond.L.Unlock()
}
// 生产者
func producer(item string) {
cond.L.Lock()
queue = append(queue, item)
cond.L.Unlock()
cond.Signal() // 通知等待者
}
使用场景:生产者-消费者模式、等待特定条件 特点:比轮询更高效的等待机制
6. sync.Map(并发安全Map) #
var m sync.Map
// 存储
m.Store("key", "value")
// 读取
if val, ok := m.Load("key"); ok {
fmt.Println(val)
}
// 删除
m.Delete("key")
// 遍历
m.Range(func(key, value interface{}) bool {
fmt.Println(key, value)
return true // 继续遍历
})
使用场景:高并发的map操作 特点:内置分段锁,无需手动加锁
7. sync.Pool(对象池) #
var pool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
// 获取对象
buf := pool.Get().(*bytes.Buffer)
buf.Reset() // 清空重用
// 使用buffer
buf.WriteString("hello")
// 使用完归还
pool.Put(buf)
使用场景:频繁创建销毁的临时对象 特点:减少GC压力,但不保证对象一定被缓存
8. atomic(原子操作) #
var counter int64
// 原子增加
atomic.AddInt64(&counter, 1)
// 原子读取
val := atomic.LoadInt64(&counter)
// 原子设置
atomic.StoreInt64(&counter, 100)
// CAS操作
atomic.CompareAndSwapInt64(&counter, 100, 200)
使用场景:简单的计数器、标志位 特点:无锁,性能最高
高级锁模式 #
分段锁(Sharded Lock) #
import "hash/fnv"
type ShardedMap struct {
shards [16]struct {
mu sync.RWMutex
m map[string]interface{}
}
}
func NewShardedMap() *ShardedMap {
sm := &ShardedMap{}
for i := range sm.shards {
sm.shards[i].m = make(map[string]interface{})
}
return sm
}
func (s *ShardedMap) getShard(key string) int {
h := fnv.New32a()
h.Write([]byte(key))
return int(h.Sum32() % 16)
}
func (s *ShardedMap) Set(key string, value interface{}) {
idx := s.getShard(key)
s.shards[idx].mu.Lock()
defer s.shards[idx].mu.Unlock()
s.shards[idx].m[key] = value
}
使用场景:超高并发的map访问 特点:将锁分散,减少竞争
自旋锁(Spinlock) #
// 注意:Go标准库没有自旋锁,这是自定义实现
type Spinlock struct {
flag int32
}
func (s *Spinlock) Lock() {
for !atomic.CompareAndSwapInt32(&s.flag, 0, 1) {
runtime.Gosched() // 让出CPU时间片
}
}
func (s *Spinlock) Unlock() {
atomic.StoreInt32(&s.flag, 0)
}
使用场景:锁持有时间极短的场景(<10μs) 特点:不睡眠,忙等待 注意:Go推荐使用channel而非自旋锁
锁的选择决策树 #
需要并发控制?
├─ 只是计数?→ atomic
├─ 初始化一次?→ sync.Once
├─ 等待多个goroutine?→ sync.WaitGroup
├─ 需要条件等待?→ sync.Cond
├─ Map操作?
│ ├─ 读多写少?→ RWMutex + map
│ └─ 高并发?→ sync.Map
├─ 对象复用?→ sync.Pool
└─ 一般互斥?
├─ 读多写少?→ sync.RWMutex
└─ 简单场景?→ sync.Mutex
性能对比 #
| 锁类型 | 相对性能 | 适用场景 |
|---|---|---|
| atomic | ★★★★★ | 简单数值操作 |
| 自旋锁 | ★★★★☆ | 极短临界区 |
| RWMutex(读) | ★★★★☆ | 读多写少 |
| sync.Map | ★★★☆☆ | 并发Map |
| Mutex | ★★★☆☆ | 一般互斥 |
| RWMutex(写) | ★★☆☆☆ | 写时性能较差 |
| Cond | ★★☆☆☆ | 条件等待 |
死锁预防原则 #
- 固定加锁顺序:多个锁时,所有goroutine按相同顺序加锁
- 避免嵌套锁:尽量不在持有锁时再获取其他锁
- 使用defer解锁:确保锁一定会释放
- 设置超时:使用context或TryLock避免无限等待
- 最小化临界区:锁内只做必要操作
记住:选对锁类型,性能差异可达10倍以上