改变一个数值的三个步骤
- 把想修改的数值从某个地方取出来
- 将取出来的数值修改为期望值
- 把修改后的数值保存到原来的地方
问题
如果在做第2步时,有另一个过程(进程或线程)对同一个数值进行同样的操作(取值、修改),那么当这两个过程都要做第3步的时候,就肯定有一个过程是白干活的。
悲观锁
悲观的锁总认为会发生并发问题,属于保守派。
如果想修改一个数值,立马给这个数值上一把锁,标明这个数值正在被修改,谁也不能修改了;然后才开始三步走,在三步走的过程结束以后,再把锁解除。
当有其他过程想要修改同一个数值时,看到了锁就不进行三步走了,而是选择等待;当锁被解除了,自己在数值也加一把锁,然后开始三步走,在三个步骤走完了,也把锁解除。
乐观锁
乐观的锁总认为不会发生并发问题,属于乐天派。
修改数据时不加锁,正常进行1、2步,在进行第3步的时候,确认一下数值是否进行了修改,如果被修改过,放弃修改,重新走一遍1、2、3步(或者放弃对数值进行修改)。
Go语言中的乐观锁与悲观锁
sync/atomic
Go语言有一个atomic包,可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作,这个包应用的便是乐观锁的原理
但是这个包只支持int32/int64/uint32/uint64/uintptr这几种数据类型的一些基础操作,如增减、交换、载入、存储等
sync
Go语言中的sync包提供了各种锁,如果使用了这个包,基本就以悲观锁的工作模式了
go代码示例
package mainimport ("fmt""sync""sync/atomic""time"
)var (x int64mu sync.Mutexwg sync.WaitGroup
)// 普通函数, 并发不安全
func Add() {x++wg.Done()
}// 互斥锁, 并发安全,性能低于原子操作
func muAdd() {mu.Lock()x++mu.Unlock()wg.Done()
}// 原子操作,并发安全,性能高于互斥锁,只针对go中的一些基本数据类型使用
func AmAdd() {atomic.AddInt64(&x, 1)wg.Done()
}func main() {// 原子操作atomic包// 加锁操作涉及到内核态的上下文切换, 比较耗时,代价高// 针对基本数据类型我们还可以使用原子操作来保证并发安全// 因为原子操作是go语言提供的方法,我们在用户态就可以完成,因此性能比加锁操作更好// go语言的原子操作由内置的库,sync/atomic完成start := time.Now()for i := 0; i < 10000; i++ {wg.Add(1)go Add() // 普通版Add函数不是并发安全的// go muAdd() // 加锁版Add函数,是并发安全的, 但是加锁性能开销大// go AmAdd() // 原子操作版Add函数,是并发安全的,性能优于加锁版}end := time.Now()wg.Wait()fmt.Println(x)fmt.Println(end.Sub(start))}
参考博客1
参考博客2