面试题答案
一键面试相同点
- 数据结构基础:两者都基于
sync.Mutex
相关的底层原语,依赖操作系统的原子操作和信号量机制来实现同步控制。 - 加锁机制:都使用原子操作来修改锁的状态,以确保在多线程环境下的安全访问。例如,通过
atomic
包中的函数来修改表示锁状态的变量。 - 解锁逻辑:解锁时都需要通知等待的 goroutine,通过
runtime_Semrelease
等函数唤醒等待队列中的 goroutine。
不同点
- 数据结构:
- Mutex:数据结构相对简单,一般就是一个表示锁状态的整数变量(如 0 表示未锁定,1 表示锁定)。
- RWMutex:数据结构更复杂,除了类似
Mutex
的写锁状态变量外,还需要额外维护读锁相关状态,例如一个表示读锁计数的变量,用于记录当前有多少个读操作持有锁。
- 加锁逻辑:
- Mutex:加锁时,直接通过原子操作尝试将锁状态从 0 改为 1。如果已经是 1,则当前 goroutine 进入等待队列。
- RWMutex:
- 读锁加锁:读锁加锁时,先检查写锁是否被持有(通过原子读取写锁状态变量),如果写锁未被持有,则原子增加读锁计数。如果写锁已被持有,则当前 goroutine 进入等待队列。
- 写锁加锁:写锁加锁时,先检查读锁计数是否为 0 以及写锁是否未被持有,只有同时满足这两个条件,才能通过原子操作获取写锁,并将写锁状态置为已锁定。否则,当前 goroutine 进入等待队列。
- 解锁逻辑:
- Mutex:解锁时,直接通过原子操作将锁状态从 1 改为 0,并唤醒等待队列中的一个 goroutine。
- RWMutex:
- 读锁解锁:读锁解锁时,原子减少读锁计数。如果读锁计数变为 0 且有写锁等待,则唤醒等待队列中的一个写锁 goroutine。
- 写锁解锁:写锁解锁时,通过原子操作将写锁状态改为未锁定,并唤醒等待队列中的所有 goroutine。
读写锁在高并发读场景下的优势实现
- 读锁并发访问:读写锁允许多个读操作同时持有读锁,因为读操作不会修改共享资源,所以不会产生数据竞争。在底层实现上,读锁加锁时只需要原子增加读锁计数,而不需要像互斥锁那样独占资源,这使得多个读操作可以并行执行,大大提高了高并发读场景下的性能。
- 写锁优先级控制:为了避免写操作饥饿,当写锁等待时,新的读锁请求不会被允许,直到写锁被释放。这通过底层检查写锁状态和读锁计数的逻辑来实现,保证了写操作能够在合理时间内获取锁并执行,维护了数据一致性。
- 读锁解锁唤醒机制:读锁解锁时,如果读锁计数变为 0 且有写锁等待,会唤醒等待队列中的一个写锁 goroutine,进一步确保了写操作的及时执行,同时又充分利用了读操作的并发特性,从而在高并发读场景下提供了较好的性能和数据一致性保证。