活锁(Livelock)vs 死锁(Deadlock)
活锁和死锁是数据库并发控制中两种不同的异常状态,虽然都可能导致事务无法正常完成,但它们的表现形式、产生原因和解决策略完全不同。
简单来说,死锁是“卡死不动”,活锁是“原地打转”。
死锁 (Deadlock):僵局
死锁就像两辆车在一条单行道上迎面相遇,谁也不肯倒车,结果谁也过不去。
- 必要条件:必须同时满足四个条件(互斥、请求与保持、不剥夺、循环等待)。
- 场景:事务T1锁定了资源A,请求资源B;同时事务T2锁定了资源B,请求资源A。T1等T2释放B,T2等T1释放A,形成死循环。
- 结局:如果不干预,这两个事务将永远等待下去。
2. 活锁 (Livelock):饥饿
活锁就像一个饥饿的人想进餐厅吃饭,但每次他走到门口,都被新来的VIP客人插队,导致他永远进不去。
- 场景:假设事务T1(低优先级)和T2(高优先级)都需要访问某个数据项。T1先请求锁,但此时系统调度器决定让T2先运行。T2释放锁后,T1正准备获取,系统又调度T2运行…如此反复,T1虽然一直在“努力”获取锁,但永远被T2抢占,导致T1无限期延迟。
- 结局:事务并没有被阻塞(Blocked),而是在运行状态(Running) 中做无用功。
自旋锁(Spin Lock)
- 线程在尝试获取锁时,若锁已被占用,不会立即阻塞,而是通过循环(自旋)不断检查锁状态,直到成功获取锁。
- 适用场景:锁竞争时间短、线程切换开销较高的场景(如高并发短任务)。
import java.util.concurrent.atomic.AtomicBoolean;
public class SpinLock {
private final AtomicBoolean lock = new AtomicBoolean(false);
// 获取锁
public void lock() {
while (!lock.compareAndSet(false, true)) {
// 自旋等待(可插入Thread.yield()减少CPU占用)
}
}
// 释放锁
public void unlock() {
lock.set(false);
}
}- 优点:无上下文切换开销,响应速度快。
- 缺点:长时间自旋会占用CPU资源,导致“忙等待”。
互斥锁(Mutex Lock)
- 线程在尝试获取锁时,若锁已被占用,会主动阻塞并让出CPU,由操作系统调度其他线程运行。
- 适用场景:锁竞争时间长、线程切换开销较低的场景(如I/O操作、复杂计算)。
// 使用 synchronized
public class SynchronizedMutex {
public synchronized void criticalSection() {
// 临界区代码
}
}
// 使用 ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockMutex {
private final Lock lock = new ReentrantLock();
public void criticalSection() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
}
}- 优点:不占用CPU资源,适合长时间等待。
- 缺点:上下文切换开销较大,响应延迟较高。
XS锁(eXclusive-Shared Lock)
XS锁是一种由排他锁(Exclusive Lock, X锁)和共享锁(Shared Lock, S锁)组成的锁机制,常见于数据库系统(如MySQL InnoDB的行级锁)和文件系统中。
工作模式
- X锁(排他锁):用于写操作,同一时刻仅允许一个事务/线程持有锁。持有X锁时,其他事务/线程无法获取任何类型的锁(包括X锁和S锁)。
- S锁(共享锁):用于读操作,允许多个事务/线程同时持有S锁。持有S锁时,其他事务/线程仍可获取S锁,但无法获取X锁。
应用场景
- 数据库事务的并发控制(如行级锁、表级锁)。
- 文件系统的读写同步(例如多个进程读取同一文件时加共享锁,写入时加排他锁)。
读写锁(Read-Write Lock)
读写锁是多线程编程中的一种同步机制,分为读锁(Read Lock)和写锁(Write Lock),与XS锁的共享-排他模式高度相似,但更侧重于线程级资源同步。
工作模式
- 读锁(共享锁):允许多个线程同时获取读锁,适用于只读操作。
- 写锁(排他锁):同一时刻仅允许一个线程获取写锁,且获取写锁时不允许其他线程持有读锁或写锁。
应用场景
- 多线程编程中保护共享数据结构(如Java的
ReentrantReadWriteLock、C++的std::shared_mutex)。 - 缓存系统的读写优化(高频读取、低频写入场景)。
XS锁 vs 读写锁
- 相同点:均基于“共享读、排他写”的设计,通过区分读/写操作提升并发性能。
- 不同点:
- XS锁是数据库/文件系统层的锁机制,服务于事务和数据一致性;
- 读写锁是线程级的同步工具,用于优化多线程程序的资源访问效率。
在实际应用中,两者可以视为同一思想在不同层级(数据库 vs. 线程)的体现。理解其异同有助于在开发中合理选择锁机制,平衡并发性能与数据一致性。
意向锁(Intention Lock)
意向锁(Intention Lock)是数据库管理系统(DBMS)中一种辅助性的锁机制,它本身并不直接锁定数据,而是表明在某个层次上有意向进行某种操作。它的核心作用是解决多粒度锁(Multiple Granularity Locking)场景下的锁冲突检测效率问题。
为什么需要意向锁
在数据库中,锁定的对象可以是数据库、表、页、行等不同粒度。如果没有意向锁,当你想给整个表加锁时,系统必须逐行扫描该表的所有行,检查是否有行级锁与表级锁冲突。这种检查方式在数据量巨大时效率极低。
意向锁的作用:通过在高层级(如表级)设置一个“标记”,来快速判断低层级(如行级)是否存在冲突的锁。它就像一个“预告”,告诉系统:“我打算在这个表里做一些操作,请检查一下表级是否允许。”
意向锁的类型
意向锁通常分为两种,分别对应共享锁(S)和排他锁(X):
- 意向共享锁(IS, Intention Shared Lock)
- 含义:表明事务打算在表中的某些行上设置共享锁。
- 场景:例如,事务想要读取表中的几行数据。它先在表上加 IS 锁,表示“我准备读几行”,然后再去锁定具体的行。
- 意向排他锁(IX, Intention Exclusive Lock)
- 含义:表明事务打算在表中的某些行上设置排他锁。
- 场景:例如,事务想要修改表中的几行数据。它先在表上加 IX 锁,表示“我准备改几行”,然后再去锁定具体的行。
意向锁的兼容性矩阵
意向锁的引入,使得锁的兼容性变得更加复杂。下表展示了表级锁(包括意向锁)之间的兼容关系:
| 当前持有的锁 | 请求的锁类型 | |||
|---|---|---|---|---|
| IS | IX | S | X | |
| IS | ✅ 兼容 | ✅ 兼容 | ✅ 兼容 | ❌ 冲突 |
| IX | ✅ 兼容 | ✅ 兼容 | ❌ 冲突 | ❌ 冲突 |
| S | ✅ 兼容 | ❌ 冲突 | ✅ 兼容 | ❌ 冲突 |
| X | ❌ 冲突 | ❌ 冲突 | ❌ 冲突 | ❌ 冲突 |
关键点
IS 与 IX 兼容:一个事务想读(IS),另一个事务想写(IX),只要它们操作的不是同一行,理论上可以并发进行,所以意向锁是兼容的。
IX 与 S 冲突:如果一个事务持有表级的 S 锁(全表只读),另一个事务想在表上加 IX 锁(打算修改某些行),这是不允许的。因为 S 锁要求全表不能修改,而 IX 锁预示着有修改意向,所以冲突。
实际应用示例
假设有两个事务 T1 和 T2 操作同一张表 Students:
- T1 开始:T1 想要修改
Students表中 ID=1 的行。- T1 首先在
Students表上加 IX 锁(意向排他锁)。 - 然后在 ID=1 的行上加 X 锁(排他锁)。
- T1 首先在
- T2 开始:T2 想要读取
Students表中 ID=2 的行。- T2 首先在
Students表上加 IS 锁(意向共享锁)。 - 锁检查:系统检查表级锁。发现当前表上有 T1 的 IX 锁。根据兼容性矩阵,IS 与 IX 是兼容的,所以 T2 的 IS 锁请求成功。
- T2 然后在 ID=2 的行上加 S 锁(共享锁),操作成功。
- T2 首先在
结果:T1 和 T2 实现了并发操作,T1 修改行1,T2 读取行2,互不干扰。
总结
意向锁是一种**“声明式”的锁**,它通过在高层级(如表)设置标记,避免了系统在加锁或冲突检测时进行全表扫描,极大地提升了数据库的并发性能。它是实现行级锁与表级锁高效共存的关键技术。
控制锁粒度的策略
拆分锁(Lock Splitting)
将一个大锁拆分为多个小锁,减少竞争。
// 错误示例:整个对象加锁(粗粒度)
public class Account {
private int balance;
public synchronized void transfer(Account target, int amount) {
// 操作余额
}
}
// 正确示例:拆分锁(细粒度)
public class Account {
private final Object lock = new Object(); // 每个账户独立锁
private int balance;
public void transfer(Account target, int amount) {
// 按固定顺序获取锁(避免死锁)
Object firstLock = System.identityHashCode(this) < System.identityHashCode(target)
? this.lock : target.lock;
Object secondLock = firstLock == this.lock ? target.lock : this.lock;
synchronized (firstLock) {
synchronized (secondLock) {
this.balance -= amount;
target.balance += amount;
}
}
}
}分段锁(Striping)
将数据分块,每块独立加锁(如 ConcurrentHashMap 的实现)。
public class StripedCounter {
private static final int N_LOCKS = 16;
private final Object[] locks = new Object[N_LOCKS];
private final int[] counts = new int[N_LOCKS];
public StripedCounter() {
for (int i = 0; i < N_LOCKS; i++) {
locks[i] = new Object();
}
}
public void increment(int key) {
int stripe = key % N_LOCKS;
synchronized (locks[stripe]) {
counts[stripe]++;
}
}
}读写锁(ReadWriteLock)
区分读锁(共享)和写锁(排他),提升读多写少场景的性能。
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteCache {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Map<String, Object> cache = new HashMap<>();
public Object get(String key) {
rwLock.readLock().lock();
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(String key, Object value) {
rwLock.writeLock().lock();
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}无锁编程(Lock-Free)
使用原子类(AtomicXXX)或CAS操作避免锁。
import java.util.concurrent.atomic.AtomicInteger;
public class LockFreeCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 无锁操作
}
}Java的原子类也是通过三种机制实现的原子操作
1. CAS(Compare-And-Swap)机制
这是最核心的实现方式,底层基于CPU的原子指令:
// AtomicInteger的源码核心
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}CAS操作原理:
- 比较当前值是否等于期望值
- 如果相等,则更新为新值
- 如果不相等,说明被其他线程修改过,操作失败
详情请查看: CAS
2. volatile + CAS组合
原子类通过volatile保证可见性,CAS保证原子性:
public class AtomicInteger extends Number implements Serializable {
private volatile int value; // volatile保证可见性
public final int get() {
return value; // 直接读取volatile变量
}
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}3. 自旋锁(Spin Lock)
当CAS失败时,采用循环重试
// Unsafe.getAndAddInt的实现类似逻辑
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset); // 获取当前值
} while (!compareAndSwapInt(o, offset, v, v + delta)); // CAS失败则重试
return v;
}