go与乐观锁和悲观锁
乐观锁和悲观锁是两种在并发编程中常用的锁机制,它们的主要目的是防止多个进程或线程同时操作共享资源时发生数据竞争或不一致的情况。下面我们将讨论这两种锁的概念以及在 Golang 中的实现。
1. 悲观锁
概念
- 悲观锁(Pessimistic Lock) 假设在操作共享资源时,会有数据竞争的情况,因此在每次操作共享资源之前都会先加锁,确保其他线程不能同时访问该资源。
- 在使用悲观锁时,当一个线程获取锁之后,其他所有试图访问该资源的线程都会被阻塞,直到锁被释放。
应用场景
- 高冲突环境:悲观锁适用于并发冲突较高的场景,比如在数据库操作中,频繁的读写可能导致数据竞争。
在 Golang 中的实现
-
在 Golang 中,悲观锁通常通过
sync.Mutex
和sync.RWMutex
实现。sync.Mutex
示例:var mu sync.Mutex var count int func increment() { mu.Lock() defer mu.Unlock() count++ }
在上面的例子中,
mu.Lock()
会阻止其他 Goroutine 访问count
变量,直到mu.Unlock()
被调用为止。sync.RWMutex
示例:var rwMu sync.RWMutex var data int func readData() int { rwMu.RLock() defer rwMu.RUnlock() return data } func writeData(newData int) { rwMu.Lock() defer rwMu.Unlock() data = newData }
在上面的例子中,
RWMutex
允许多个 Goroutine 同时读操作(使用RLock()
),但写操作(使用Lock()
)会阻塞其他读和写操作。
2. 乐观锁
概念
- 乐观锁(Optimistic Lock) 假设在操作共享资源时,数据竞争是非常少见的,因此在操作资源时不立即加锁,而是在提交操作前检查资源是否发生了变化。如果资源没有变化,则提交操作;如果资源已经被修改,则操作失败或重试。
- 乐观锁通常使用版本号或时间戳来检测资源是否被修改。
应用场景
- 低冲突环境:乐观锁适用于并发冲突较低的场景,比如在数据库操作中,读多写少的情况下。
在 Golang 中的实现
-
在 Golang 中,乐观锁的实现通常依赖于 CAS(Compare-And-Swap)操作。Go 的标准库
sync/atomic
提供了一些原子操作函数,可以用来实现乐观锁。sync/atomic
示例:import ( "sync/atomic" ) var counter int64 func increment() { for { oldValue := atomic.LoadInt64(&counter) newValue := oldValue + 1 if atomic.CompareAndSwapInt64(&counter, oldValue, newValue) { break } } }
在上面的例子中,
CompareAndSwapInt64
是一个典型的乐观锁操作。如果counter
的值在读取后没有变化,那么CompareAndSwapInt64
会将其更新为新值,并返回true
,否则返回false
并重试。
3. 悲观锁 vs. 乐观锁
-
悲观锁:
- 优势:能有效防止并发冲突,适用于高并发写操作的场景。
- 劣势:可能导致较高的锁争用,降低系统的并发性能。
-
乐观锁:
- 优势:在并发冲突较低时,性能更好,适用于读多写少的场景。
- 劣势:在冲突频繁时,可能导致大量重试操作,反而降低性能。
4. 总结
- 悲观锁:通过在操作共享资源时主动加锁来避免并发冲突,适用于高冲突的环境。在 Go 中通过
sync.Mutex
和sync.RWMutex
实现。 - 乐观锁:假设并发冲突不频繁,通过在操作完成时检查冲突来避免数据不一致,适用于低冲突的环境。在 Go 中可以通过
sync/atomic
提供的 CAS 操作来实现。