4. 锁的基础分类:乐观锁与悲观锁、公平锁与非公平锁、可重入锁与不可重入锁、共享锁与排他锁

在 SSO 单点登录系统中,多个异步任务(如令牌刷新、会话同步、票据校验)需要并发访问共享资源(如用户会话缓存、令牌黑名单、分布式计数器)。锁是控制并发访问的核心机制。理解锁的基础分类,是设计高效、正确并发控制方案的前提。

4.1 乐观锁与悲观锁

这是从并发冲突的应对策略角度进行的分类,决定了锁的加锁时机与粒度。

特性 乐观锁 (Optimistic Lock) 悲观锁 (Pessimistic Lock)
核心思想 假设并发冲突很少发生,先进行操作,提交时再检查冲突。 假设并发冲突一定会发生,在操作前就加锁阻塞其他线程。
实现方式 版本号机制、CAS(Compare And Swap)算法。 数据库行锁、synchronized、ReentrantLock。
适用场景 读多写少、冲突概率低的场景(如用户信息读取)。 写多读少、冲突概率高的场景(如令牌发放计数器)。
优缺点 无锁开销,吞吐量高;但需要处理重试或回滚。 保证强一致性;但锁竞争激烈时性能下降,易死锁。
SSO 实战提示: 在 SSO 的会话缓存更新中,推荐使用乐观锁(如 Redis 的 WATCH 命令或版本号)来更新用户属性,避免长时间持有锁导致令牌发放延迟。仅在操作令牌黑名单等写冲突极高的资源时,才使用悲观锁。
乐观锁 vs 悲观锁 执行流程对比 乐观锁:无锁读取 + 版本校验 1. 读取数据 & 版本号 V1 2. 执行业务逻辑(无锁) 3. 提交:检查版本号 V1 == V2? 失败:重试 成功:提交 悲观锁:先加锁再操作 1. 尝试获取锁(阻塞等待) 2. 获取锁成功,读取数据 3. 执行业务逻辑(独占) 4. 释放锁,唤醒等待线程 说明:乐观锁适用于冲突少的场景,悲观锁适用于写密集场景

4.2 公平锁与非公平锁

这是从线程获取锁的顺序角度进行的分类,直接影响线程的调度公平性和系统吞吐量。

  • 公平锁 (Fair Lock): 按照线程请求锁的先后顺序(FIFO)分配锁。所有线程都会排队,不会发生“插队”现象。
  • 非公平锁 (Non-fair Lock): 允许线程在锁释放时“插队”。如果锁刚好被释放,新来的线程可能直接获取锁,而唤醒队列中的线程。
对比维度 公平锁 非公平锁
吞吐量 较低(线程切换频繁) 较高(减少线程挂起/唤醒开销)
公平性 严格保证,无饥饿 不保证,可能导致线程饥饿
实现代价 需要维护精确的等待队列 实现简单,CAS 直接竞争
SSO 推荐 令牌发放等关键路径,避免某个请求长时间等待 会话缓存批量更新,追求高吞吐
避坑指南: 在 SSO 的异步任务编排中,如果使用非公平锁处理令牌刷新任务,可能导致低优先级的刷新任务长时间“插队”,使得高优先级的登录请求因获取不到锁而超时。建议对关键路径使用公平锁,或使用优先级队列配合锁机制。

4.3 可重入锁与不可重入锁

这是从同一线程能否重复获取同一把锁的角度进行的分类。

  • 可重入锁 (Reentrant Lock): 允许同一个线程多次获取同一把锁。锁内部维护一个持有计数器和所有者线程。每次重入计数器 +1,每次释放 -1,直到计数器归零才真正释放锁。
  • 不可重入锁 (Non-reentrant Lock): 同一线程在持有锁的情况下,再次尝试获取该锁会导致死锁(自己锁住自己)。
代码示例:可重入锁 vs 不可重入锁行为
// 可重入锁(Java ReentrantLock)
ReentrantLock lock = new ReentrantLock();
public void outer() {
    lock.lock();
    try {
        inner(); // 再次获取同一把锁,成功
    } finally {
        lock.unlock();
    }
}
public void inner() {
    lock.lock(); // 可重入:计数器 +1
    try {
        // 执行内部逻辑
    } finally {
        lock.unlock(); // 计数器 -1
    }
}

// 不可重入锁(自定义简单实现)
public class NonReentrantLock {
    private boolean locked = false;
    public synchronized void lock() {
        while (locked) {
            wait(); // 如果已锁,阻塞
        }
        locked = true; // 假设线程A持有锁
        // 如果线程A再次调用 lock(),locked 为 true,自己阻塞自己 → 死锁
    }
}

在 SSO 的异步任务中,一个任务可能调用多个子方法(如 validateToken() 内部调用 refreshSession()),如果这些方法都使用同一把锁,必须使用可重入锁,否则会导致死锁。

4.4 共享锁与排他锁

这是从锁的访问权限角度进行的分类,决定了多个线程能否同时读取或写入。

锁类型 读-读 读-写 写-写 典型实现
共享锁 (S锁 / Read Lock) 允许 阻塞 阻塞 ReentrantReadWriteLock.ReadLock
排他锁 (X锁 / Write Lock) 阻塞 阻塞 阻塞 ReentrantLock、synchronized
  • 共享锁: 多个线程可以同时持有共享锁,只要没有线程持有排他锁。适用于读多写少的场景,极大提升并发读性能。
  • 排他锁: 同一时刻只能有一个线程持有该锁,其他线程无论是读还是写都必须等待。保证数据写入的强一致性。
共享锁与排他锁的并发访问模型 共享锁 (Read Lock) — 读读不互斥 线程A 读 线程B 读 线程C 读 ✅ 多个读线程可以同时访问 排他锁 (Write Lock) — 写写/读写互斥 线程D 写(持有锁) 线程E 读(阻塞) ❌ 写操作独占,其他读写全部阻塞 核心原则:读读共享,读写互斥,写写互斥 SSO 应用:会话缓存使用读写锁,令牌发放使用排他锁

4.5 锁分类在 SSO 异步任务中的综合选型

在 SSO 单点登录的异步任务编排中,不同锁类型需要组合使用。以下是一个典型的选型对照表:

SSO 资源 推荐锁类型 理由
用户会话缓存(读多写少) 乐观锁 + 共享锁 减少锁竞争,提升并发读取性能
令牌黑名单(写多) 悲观锁 + 排他锁 保证黑名单的强一致性,防止重复校验
分布式计数器(如登录失败次数) 乐观锁(CAS) 短操作,冲突概率低,避免锁开销
异步任务队列调度 公平锁 + 可重入锁 防止任务饥饿,支持任务内部嵌套调用
全局配置更新 排他锁 + 悲观锁 配置变更频率低,但必须原子生效
设计原则: 在 SSO 异步任务编排中,优先使用乐观锁和共享锁提升吞吐量;仅在数据一致性要求极高或写冲突严重时,才退化为悲观锁和排他锁。同时,所有涉及递归或嵌套调用的锁,必须使用可重入锁。