基于本文回答
0
评论

Java公平锁和非公平锁的区别

知识点图片

本文讲解Java公平锁与非公平锁。公平锁如排队,先到先得,性能较低;非公平锁可“插队”,性能更高但可能导致线程饥饿。ReentrantLock可配置此策略。

我们来详细讲解一下Java中的公平锁和非公平锁。这是一个在Java并发编程中非常重要的概念,主要与ReentrantLock相关。

1. 核心概念:什么是公平与非公平?

想象一下在银行排队办业务。

  • 公平锁 (Fair Lock):就像严格排队。每个新来的人都必须到队尾排队,只有排在最前面的人才能办理业务。这种方式非常公平,保证了“先来后到”(First-In, First-Out, FIFO)。等待时间最长的线程会最先获得锁。

  • 非公平锁 (Non-fair Lock):就像允许“插队”。当一个窗口空闲出来时(锁被释放),一个刚到银行的人(新来的线程)可以不用去排队,直接尝试抢占这个窗口。如果他抢到了,就直接办理业务。只有当他没抢到时,才会乖乖去队尾排队。这种方式不保证先来后到,可能会导致队列中某些人(线程)等待很长时间。


2. Java中的实现:ReentrantLock

在Java中,公平锁和非公平锁是java.util.concurrent.locks.ReentrantLock类的两种实现策略。我们可以通过其构造函数来选择:

java
// 1. 创建一个非公平锁(默认行为)
Lock nonFairLock = new ReentrantLock(); 
// 等价于:
Lock nonFairLock = new ReentrantLock(false);

// 2. 创建一个公平锁
Lock fairLock = new ReentrantLock(true); // 传入true

为什么默认是非公平的?
因为非公平锁通常有更高的性能。我们稍后会详细解释原因。


3. 公平锁与非公平锁的详细对比

特性/方面 公平锁 (Fair Lock) 非公平锁 (Non-fair Lock)
获取锁的规则 严格按照线程请求锁的顺序(FIFO队列)分配。 允许新来的线程“插队”,直接尝试获取锁。如果获取失败,再进入等待队列。
性能/吞吐量 较低。因为线程切换开销大。即使锁是可用的,也需要检查队列头部是否有等待者,并可能需要唤醒等待的线程,这个过程有延迟。 较高。减少了线程挂起和唤醒的次数。一个线程释放锁后,另一个刚请求的线程可以立即获取锁,无需等待队列中的线程被唤醒,从而提高了吞吐量。
饥饿问题 不会产生饥饿。因为有严格的排队机制,每个线程最终都有机会获得锁。 可能产生饥饿。如果一个线程运气总是不好,每次它被唤醒时,都有新来的线程“插队”成功,那么它可能永远也得不到锁。
实现原理 (AQS) 线程请求锁时,会先检查等待队列中是否有比它更早的线程。如果有,则获取失败并进入队列。 线程请求锁时,会先用CAS(Compare-And-Swap)操作尝试直接获取锁。如果成功,就“插队”了。如果失败,再走公平锁的逻辑(检查队列并排队)。

4. 源码层面的简单解析 (基于AQS)

ReentrantLock的公平性是其内部的Sync类(继承自AbstractQueuedSynchronizer,简称AQS)决定的。

非公平锁 lock() 方法的逻辑:

java
// NonfairSync.java
final void lock() {
    // 1. 尝试直接抢锁(插队的关键)
    if (compareAndSetState(0, 1)) {
        setExclusiveOwnerThread(Thread.currentThread());
    } else {
        // 2. 如果抢锁失败,再走标准的获取流程(可能会排队)
        acquire(1);
    }
}

它会先尝试一次CAS操作来抢锁。如果成功了,就直接拿到了锁,实现了“插队”。

公平锁 lock() 方法的逻辑:

java
// FairSync.java
final void lock() {
    acquire(1);
}

// acquire(1) 内部会调用 tryAcquire
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 关键区别在这里:!hasQueuedPredecessors()
        // 在尝试获取锁之前,会检查队列中是否有比自己更早的等待者
        if (!hasQueuedPredecessors() && compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // ... 处理重入的逻辑 ...
    return false;
}

公平锁在尝试获取锁之前,会调用hasQueuedPredecessors()方法,检查AQS的等待队列中是否已经有其他线程在等待了。如果有,它就不会去尝试获取锁,而是乖乖地去排队,从而保证了公平性。


5. 性能差异的原因(为什么非公平锁更快?)

非公平锁的性能优势主要来自于减少了CPU上下文切换

  • 场景: 线程A释放了锁,此时线程B在等待队列的头部,正等待被唤醒。
  • 公平锁: 系统必须先唤醒线程B,让它获取锁。这个唤醒过程涉及到线程状态的转换(从WAITING到RUNNABLE),需要时间。
  • 非公平锁: 如果此时线程A(刚释放锁的那个线程)又立即请求这把锁(或者另一个CPU上正在运行的线程C请求这把锁),它可以直接尝试获取。由于它正在CPU上运行,所以获取锁的概率很大,并且几乎没有延迟。它避免了唤醒线程B的开销。虽然对线程B不公平,但整体的吞吐量(单位时间内完成的任务数)提高了。

6. 如何选择?

  • 使用非公平锁(默认)

    • 绝大多数情况下,性能是首要考虑因素
    • 业务逻辑对线程获取锁的顺序没有严格要求。
    • 线程持有锁的时间很短,或者线程竞争不激烈,发生饥饿的概率很低。
    • 这是ReentrantLocksynchronized的默认策略,足以说明它的普适性。
  • 使用公平锁

    • 公平性是业务的硬性要求时。例如,实现一个资源调度系统,必须保证先申请的先得到服务。
    • 当需要避免线程饥饿问题时。
    • 注意:使用公平锁会带来显著的性能开销,所以只有在确实需要时才使用。

7. synchronized 是公平的还是非公平的?

Java中的synchronized关键字实现的是非公平锁

它是一种内置的、由JVM实现的锁机制。在Java 6之后,JVM对synchronized进行了大量优化(如偏向锁、轻量级锁、自旋锁等),但当竞争激烈,锁升级为重量级锁时,其行为仍然是非公平的,不保证线程的FIFO顺序。你无法将其设置为公平锁。

总结

特性 公平锁 (new ReentrantLock(true)) 非公平锁 (new ReentrantLock()synchronized)
核心思想 先来后到,严格排队 允许插队,效率优先
优点 公平,不产生饥饿 吞吐量大,性能高
缺点 性能较低,上下文切换频繁 可能导致线程饥饿
适用场景 对顺序有严格要求的业务 绝大多数追求高性能的场景
右滑查看面试常问