2787 字
14 分钟
锁总结
2025-03-19
无标签

活锁(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 锁,表示“我准备改几行”,然后再去锁定具体的行。

意向锁的兼容性矩阵#

意向锁的引入,使得锁的兼容性变得更加复杂。下表展示了表级锁(包括意向锁)之间的兼容关系:

当前持有的锁请求的锁类型
ISIXSX
IS✅ 兼容✅ 兼容✅ 兼容❌ 冲突
IX✅ 兼容✅ 兼容❌ 冲突❌ 冲突
S✅ 兼容❌ 冲突✅ 兼容❌ 冲突
X❌ 冲突❌ 冲突❌ 冲突❌ 冲突

关键点#

  • IS 与 IX 兼容:一个事务想读(IS),另一个事务想写(IX),只要它们操作的不是同一行,理论上可以并发进行,所以意向锁是兼容的。

  • IX 与 S 冲突:如果一个事务持有表级的 S 锁(全表只读),另一个事务想在表上加 IX 锁(打算修改某些行),这是不允许的。因为 S 锁要求全表不能修改,而 IX 锁预示着有修改意向,所以冲突。

实际应用示例#

假设有两个事务 T1 和 T2 操作同一张表 Students

  1. T1 开始:T1 想要修改 Students表中 ID=1 的行。
    • T1 首先在 Students表上加 IX 锁(意向排他锁)。
    • 然后在 ID=1 的行上加 X 锁(排他锁)。
  2. T2 开始:T2 想要读取 Students表中 ID=2 的行。
    • T2 首先在 Students表上加 IS 锁(意向共享锁)。
    • 锁检查:系统检查表级锁。发现当前表上有 T1 的 IX 锁。根据兼容性矩阵,IS 与 IX 是兼容的,所以 T2 的 IS 锁请求成功。
    • T2 然后在 ID=2 的行上加 S 锁(共享锁),操作成功。

结果: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;
}
锁总结
https://lettle.cn/posts/locks/
作者
Lettle
发布于
2025-03-19
许可协议
CC BY-NC-SA 4.0