RWMutex双重检查锁

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的核心规则 #

  1. 只有一个人能成功,其他人排队等待
  2. 等待的goroutine会阻塞(睡眠),不消耗CPU
  3. 锁释放时,系统会唤醒一个等待者
红包锁.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+双重检查读并发,写安全最优

核心要点 #

  1. RWMutex适用场景:读多写少
  2. 双重检查目的:防止重复初始化
  3. Lock等待机制:阻塞队列,不消耗CPU
  4. 解锁再加锁:避免死锁,但需要二次检查

常见应用 #

  • 单例模式
  • 连接池初始化
  • 配置文件加载
  • 缓存系统
  • 秒杀系统(防超卖)

常见错误 #

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★★☆☆☆条件等待

死锁预防原则 #

  1. 固定加锁顺序:多个锁时,所有goroutine按相同顺序加锁
  2. 避免嵌套锁:尽量不在持有锁时再获取其他锁
  3. 使用defer解锁:确保锁一定会释放
  4. 设置超时:使用context或TryLock避免无限等待
  5. 最小化临界区:锁内只做必要操作

记住:选对锁类型,性能差异可达10倍以上