Java并发编程:公平锁与非公平锁详解及源码分析

ReentrantLock 公平锁和非公平锁代码解析

  1. 公平锁与非公平锁的概念
    公平锁
    公平锁保证获取锁的顺序与线程请求的顺序一致,也就是说,先请求锁的线程一定会先获得锁。这种机制依赖于线程队列,当线程尝试获取锁时,它会先检查队列中是否有等待更久的线程,如果有,则该线程必须排队等待,直到队列前面的线程获取锁后释放。

非公平锁
与公平锁相对,非公平锁并不保证获取锁的顺序。当一个线程请求锁时,它可以直接尝试获取锁,而不管队列中是否有等待的线程。因此,后来的线程可能抢占到锁的机会,导致一些线程长时间得不到锁,出现线程饥饿的情况。然而,非公平锁的性能通常比公平锁高,因为它减少了队列调度的开销。

  1. 非公平锁的源码解析
    下面我们来分析 ReentrantLock 中非公平锁的核心实现。非公平锁是 ReentrantLock 的默认实现,其具体逻辑在 NonfairSync 类中。
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    // 尝试获取非公平锁
    final boolean initialTryLock() {
        Thread current = Thread.currentThread(); // 获取当前线程

        // 如果当前锁的状态是 0,尝试将其状态设置为 1,表示成功获取锁
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(current); // 将当前线程设置为锁的持有者
            return true; // 获取锁成功
        } 
        // 如果锁已经被当前线程持有,支持锁的重入
        else if (getExclusiveOwnerThread() == current) {
            int c = getState() + 1; // 锁的重入次数 +1
            if (c < 0) // 检查是否超出最大重入次数
                throw new Error("Maximum lock count exceeded");
            setState(c); // 更新锁状态
            return true;
        } 
        // 如果锁被其他线程持有,则返回 false,获取锁失败
        else
            return false;
    }
}

源码解析:
锁的初次尝试获取:

compareAndSetState(0, 1):这是一个原子操作,用于尝试将锁的状态从 0 改为 1。如果成功,则表示当前线程获取了锁,并跳过了队列中的其他等待线程。这就是非公平锁的关键部分,因为它不考虑是否有其他线程在等待。
setExclusiveOwnerThread(current):设置当前线程为锁的持有者,防止其他线程在锁释放之前获取到该锁。
支持重入锁:

如果锁已经被当前线程持有(即重入锁的情况),getExclusiveOwnerThread() == current 判断为真。然后,增加锁的重入计数,通过 setState© 来记录重入的次数。
如果锁的重入次数超过最大值,会抛出 Maximum lock count exceeded 错误。
锁获取失败:

如果锁已经被其他线程持有且当前线程并非持有者,那么返回 false,表示获取锁失败。
3. 公平锁的源码解析
与非公平锁不同,公平锁通过线程队列来确保线程获取锁的顺序。公平锁的实现是通过 FairSync 类来完成的。

FairSync 的源码分析

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;
    // 尝试获取公平锁
    final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState(); // 获取当前锁的状态

        // 如果当前锁状态为 0 且没有其他线程等待,尝试获取锁
        if (c == 0) {
            if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current); // 设置当前线程为锁的持有者
                return true;
            }
        }
        // 如果锁已经被当前线程持有,则支持重入
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // 检查是否超过最大重入次数
                throw new Error("Maximum lock count exceeded");
            setState(nextc); // 更新锁状态
            return true;
        }
        return false; // 获取锁失败
    }
}

源码解析:
公平性保证:

hasQueuedPredecessors():公平锁的核心在于它会首先检查是否有其他线程在队列中等待锁。如果有,当前线程必须排队,直到轮到自己。
compareAndSetState(0, acquires):只有当队列中没有等待线程且锁的状态为 0 时,当前线程才可以获取锁。这就保证了锁的获取顺序是按照请求顺序来的。
重入锁支持:

和非公平锁一样,公平锁同样支持重入。如果锁已经被当前线程持有,则允许再次获取,并更新锁的状态。
锁获取失败:

如果其他线程正在持有锁或者当前线程不是锁的持有者且有其他线程在等待,那么获取锁失败。
4. 使用场景对比
非公平锁的适用场景:
高性能、高吞吐量场景:非公平锁能够减少线程上下文切换的开销,因此适用于高并发、高吞吐量的场景,如数据库连接池、线程池管理等。
线程执行时间较短的场景:当每个线程的执行时间非常短时,非公平锁的抢占特性可以有效提高系统的并发性。
线程饥饿可容忍的场景:由于非公平锁可能会导致某些线程长时间得不到锁,因此适合对线程顺序和公平性要求不高的场景。
公平锁的适用场景:
公平性要求较高的场景:如果业务需要确保多个线程按照请求锁的顺序依次获得锁,那么公平锁是更合适的选择。
任务执行时间较长的场景:如果某些任务的执行时间较长,公平锁可以确保没有线程长期处于饥饿状态,避免系统中的线程等待时间过长。

  1. 如何在代码中使用非公平锁和公平锁?
    在 Java 中,ReentrantLock 支持显式创建公平锁和非公平锁。默认情况下,ReentrantLock 使用非公平锁。你可以通过构造函数的参数来决定锁的公平性。

创建非公平锁(默认)
ReentrantLock nonFairLock = new ReentrantLock();

显式创建非公平锁
ReentrantLock nonFairLock = new ReentrantLock(false);

  1. 总结
    非公平锁注重性能,它允许后来的线程插队,减少了上下文切换的开销,适用于高并发、高性能的场景。
    公平锁则严格按照线程请求锁的顺序来分配锁,保证线程的公平性,适用于需要顺序执行的场景。
    两者各有优劣,开发者需要根据具体的业务场景来选择合适的锁机制。在实际应用中,如果对性能要求更高且可以接受线程饥饿,那么非公平锁是一个更好的选择;而在一些必须保证公平性的场景下,使用公平锁则更加合理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值