🔒 互斥锁、自旋锁、读写锁、乐观锁、悲观锁的区别及适用场景
在并发编程和数据库事务控制中,锁是保证数据一致性的核心手段。不同类型的锁适用于不同的场景,理解它们的特点可以帮助我们在高并发系统中做出最佳选择。
1️⃣ 互斥锁(Mutex)
🔹 概念:
- 互斥锁(Mutual Exclusion Lock,简称Mutex)是一种用于保证同一时刻只有一个线程访问资源的锁。
- 获取锁的线程可以阻塞其他线程,直到锁被释放。
🔹 适用场景:
- 单线程独占资源(如临界区访问)
- I/O 操作(文件、网络等资源访问)
🔹 优缺点:
优点 | 缺点 |
---|---|
保证数据安全 | 线程被阻塞,发生上下文切换,影响性能 |
🔹 示例(Java ReentrantLock):
Lock lock = new ReentrantLock();
lock.lock();
try {
// 访问临界资源
} finally {
lock.unlock();
}
2️⃣ 自旋锁(Spinlock)
🔹 概念:
- 自旋锁是一种不主动让出 CPU,而是不断循环检查锁是否可用的锁。
- 适用于锁持有时间很短的情况,避免线程阻塞的开销。
🔹 适用场景:
- 多核 CPU,锁占用时间短(如 CPU 缓存同步)
- 避免线程阻塞导致的上下文切换开销
🔹 优缺点:
优点 | 缺点 |
---|---|
避免线程切换的开销(适用于锁持有时间短的情况) | 线程会占用 CPU 资源,不适用于长时间持有锁的情况 |
🔹 示例(Java 自旋锁):
while (!lock.compareAndSet(false, true)) {
// 自旋等待
}
3️⃣ 读写锁(Read-Write Lock)
🔹 概念:
- 读写锁允许多个线程同时读取(共享读),但写入时需要独占锁(独占写)。
- 适用于读多写少的场景,提高并发性。
🔹 适用场景:
- 缓存系统(Redis、数据库索引)
- 配置读取(多个线程同时读,但偶尔有写操作)
🔹 优缺点:
优点 | 缺点 |
---|---|
读操作并发性高,适用于读多写少的情况 | 写操作仍然是串行的,写入时所有读线程都需要等待 |
🔹 示例(Java ReadWriteLock):
ReadWriteLock lock = new ReentrantReadWriteLock();
// 读锁
lock.readLock().lock();
try {
// 读取数据
} finally {
lock.readLock().unlock();
}
// 写锁
lock.writeLock().lock();
try {
// 修改数据
} finally {
lock.writeLock().unlock();
}
4️⃣ 乐观锁(Optimistic Lock)
🔹 概念:
- 乐观锁假设数据不会被修改,在操作数据前不加锁,而是在更新时检查数据是否发生变化(如版本号)。
- 如果数据未被其他线程修改,则更新成功;否则,需要重试。
🔹 适用场景:
- 高并发场景(数据库更新、缓存更新)
- 数据库事务(通过版本号
version
进行控制)
🔹 优缺点:
优点 | 缺点 |
---|---|
提高并发性(不锁资源) | 需要额外的冲突检测(可能导致重试开销) |
🔹 示例(数据库版本控制):
UPDATE users
SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 5;
- 只有当
version = 5
时更新才会成功,否则表示数据已被其他线程修改,需要重试。
5️⃣ 悲观锁(Pessimistic Lock)
🔹 概念:
- 悲观锁假设数据总是会被修改,因此在操作前先加锁,防止其他线程修改数据。
- 适用于竞争激烈的场景,确保数据一致性。
🔹 适用场景:
- 数据库事务控制(行级锁、表锁)
- 高冲突资源访问(如订单扣库存)
🔹 优缺点:
优点 | 缺点 |
---|---|
保证数据一致性 | 影响并发性能,可能导致锁等待 |
🔹 示例(数据库行锁):
SELECT * FROM users WHERE id = 1 FOR UPDATE;
FOR UPDATE
语句会加行锁,其他事务必须等待该事务提交后才能访问该行数据。
🔎 总结:不同锁的适用场景
锁类型 | 特点 | 适用场景 | 缺点 |
---|---|---|---|
互斥锁(Mutex) | 线程互斥访问,阻塞等待 | I/O 操作、临界区保护 | 线程阻塞,影响性能 |
自旋锁(Spinlock) | 不让出 CPU,循环等待 | 锁时间短的高并发任务 | 占用 CPU 资源,锁时间长时性能下降 |
读写锁(Read-Write Lock) | 允许并发读,写互斥 | 读多写少的场景(如缓存) | 写操作仍然串行,影响写性能 |
乐观锁(Optimistic Lock) | 先操作,提交时检查冲突 | 高并发数据库更新 | 需要额外的重试机制 |
悲观锁(Pessimistic Lock) | 操作前加锁,防止修改 | 数据库事务、库存扣减 | 影响并发性,可能导致死锁 |
🔥 面试技巧
面试官:什么时候用乐观锁,什么时候用悲观锁?
✅ 回答要点:
- 高并发场景(如订单更新) → 用乐观锁(减少锁等待)
- 数据竞争激烈(如银行账户操作) → 用悲观锁(确保数据一致性)
面试官:自旋锁和互斥锁的区别?
✅ 回答要点:
- 互斥锁 线程会阻塞等待,发生上下文切换。
- 自旋锁 线程不阻塞,而是在循环等待(适用于锁时间短的情况)。
面试官:如何提高数据库并发?
✅ 回答要点:
- 读多写少 → 用读写锁
- 高并发更新 → 用乐观锁
- 数据一致性要求高 → 用悲观锁