公平锁和非公平锁

AQS(AbstractQueuedSynchronizer)本身既可以用来实现 公平锁,也可以用来实现 非公平锁,它提供了支持两者的基础机制。在 Java 并发框架中,具体是通过 ReentrantLock 等实现类来区分公平和非公平锁。

1. 公平锁与非公平锁的定义

  • 公平锁(Fair Lock):线程获取锁的顺序与它们请求锁的顺序一致。先请求锁的线程将优先获得锁,后请求的线程需要排队等待,遵循先进先出(FIFO)规则。
  • 非公平锁(Non-Fair Lock):线程获取锁的顺序与请求锁的顺序无关。任何线程都有可能在锁释放时立刻尝试抢占锁,即使其他线程已经在排队等待。

2. AQS 如何实现公平锁和非公平锁

AQS 本身并不直接决定公平或非公平,而是提供了基础的锁请求排队机制,具体的公平性策略是在子类中实现的。例如,在 ReentrantLock 中,可以通过构造函数选择使用公平锁或非公平锁。

2.1 公平锁的实现

公平锁 中,每个线程必须按顺序获取锁,这就要求线程在获取锁时需要首先检查 等待队列 中是否有其他线程。如果等待队列中有其他线程,则当前线程不能直接获取锁,必须进入队列等待。

实现公平锁的核心在于重写 AQStryAcquire() 方法,保证线程按照队列的顺序获取锁。

公平锁的工作流程:
  1. 线程尝试获取锁时,首先通过 hasQueuedPredecessors() 方法检查当前等待队列中是否有其他线程。
  2. 如果有其他线程等待,则当前线程不能直接获取锁,必须进入等待队列。
  3. 如果没有其他线程等待,线程可以继续通过 CAS 操作尝试获取锁。

ReentrantLock 的公平锁实现:

javaCopy codeprotected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    
    // 如果当前锁为空闲状态,则检查是否有其他线程在排队
    if (c == 0) {
        // 公平锁的关键点,判断是否有其他线程等待
        if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果当前线程已经持有锁,则可以进行重入
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        setState(nextc);
        return true;
    }
    return false;
}

hasQueuedPredecessors() 方法用于判断当前线程前是否有排队的线程,如果有的话当前线程将进入队列等待。

2.2 非公平锁的实现

非公平锁 不需要遵循队列顺序,它允许线程在任何时候直接尝试获取锁,甚至可能“插队”在等待队列前面。如果当前锁是空闲的,线程不必检查等待队列,直接通过 CAS 操作尝试获取锁。

非公平锁的工作流程:

  1. 线程首先直接尝试获取锁,不考虑等待队列中的其他线程。
  2. 如果锁是空闲的,当前线程立即获取锁,不论是否有其他线程在等待。
  3. 如果锁被占用,线程将进入等待队列,直到锁释放。

ReentrantLock 的非公平锁实现:

javaCopy codeprotected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    
    // 不检查等待队列,直接尝试获取锁
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果当前线程已经持有锁,则允许重入
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        setState(nextc);
        return true;
    }
    return false;
}

在非公平锁中,线程通过 nonfairTryAcquire() 方法直接尝试获取锁,而不会检查队列中的其他线程是否在等待,这种方式提高了锁的竞争效率。

3. 公平锁和非公平锁的区别

3.1 公平性

  • 公平锁:线程按照请求锁的先后顺序获取锁,避免了线程“插队”的现象,确保先请求的线程先得到锁。这适合需要强顺序性的场景,但会导致线程上下文切换频繁,性能略有损耗。
  • 非公平锁:线程直接尝试获取锁,无需等待队列中排在前面的线程释放锁,可能会出现“插队”的情况。这种锁的吞吐量较高,因为允许线程更快地获得锁,减少了排队的开销,但可能导致某些线程长期得不到锁,产生 线程饥饿 问题。

3.2 性能

  • 公平锁:由于需要检查等待队列,公平锁会增加额外的上下文切换开销,导致性能下降,尤其在高并发场景下。
  • 非公平锁:非公平锁减少了上下文切换的开销,性能更高,特别是在并发量大的情况下,锁的获取速度更快。

3.3 场景适用性

  • 公平锁:适用于需要严格控制访问顺序的场景。例如:一些金融系统、银行交易系统中,需要保证事务的严格顺序性。
  • 非公平锁:适用于追求性能、并发高的场景。例如:大多数 Web 应用、缓存系统等,允许线程快速获取锁,以提高吞吐量。

4. 公平锁和非公平锁的选择

4.1 公平锁适用场景

  • 需要严格按照请求顺序获取锁的业务场景。
  • 长时间运行的任务可能会受到锁竞争影响的情况。

公平锁可以避免某些线程长期得不到锁(线程饥饿问题),但可能会降低吞吐量。适合对顺序性要求高的场景,例如:

  • 银行转账系统。
  • 订单处理系统。
  • 需要保证资源分配顺序的系统。

4.2 非公平锁适用场景

  • 追求高吞吐量、需要极大提升性能的业务场景。
  • 线程饥饿不是问题,允许线程快速抢占锁的场景。

非公平锁适合于高并发、性能敏感的场景,例如:

  • Web 服务器处理大量并发请求。
  • 缓存系统中,大量线程频繁获取和释放锁。
  • 高性能计算或需要减少上下文切换的系统。

5. 总结

  • 公平锁:按照线程请求顺序获取锁,避免线程饥饿,适合需要严格控制访问顺序的场景,但性能相对较低。
  • 非公平锁:允许线程插队,性能更高,适合高并发场景,但可能导致线程饥饿。

在 Java 中,ReentrantLock 通过构造函数提供了公平锁和非公平锁的选择:

javaCopy codeReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock nonFairLock = new ReentrantLock(false); // 非公平锁

选择哪种锁,主要取决于应用场景的需求:如果系统需要严格保证公平性,选择公平锁;如果需要更高的性能和吞吐量,选择非公平锁。

0 0 投票数
Article Rating
订阅评论
提醒
guest
0 评论
最旧
最新 最多投票
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x