公平锁与非公平锁的区别及其在 ReentrantLock 中的实现详解

116 篇文章 2 订阅
引言

在并发编程中,锁是用于解决多个线程对共享资源进行竞争访问的常见工具。锁机制的设计可以分为多种形式,其中最为常见的是公平锁非公平锁。Java 提供了 ReentrantLock 来实现这两种锁的机制,通过使用公平和非公平锁,我们可以对线程竞争进行不同的控制。

公平锁保证锁的获取按照线程请求的顺序进行,而非公平锁则允许线程抢占锁。虽然公平锁可以避免线程饥饿,但它也带来了性能上的开销。本文将详细讲解公平锁与非公平锁的区别,探讨公平锁的缺点,并通过代码和图文解释 ReentrantLock 是如何实现这两种锁的。


第一部分:什么是公平锁与非公平锁

1.1 公平锁

公平锁是指线程获取锁的顺序严格按照它们请求锁的顺序进行。即每个线程获取锁的顺序与它们排队的顺序保持一致,类似于在银行排队取号的机制。公平锁的实现通过一个先进先出的队列(FIFO)来管理线程,当某个线程持有锁时,其他线程会进入等待队列,直到轮到自己。

特点

  • 保证每个线程都能够公平地获得锁。
  • 避免了线程饥饿问题。

示意图:公平锁的锁获取流程

+-------------------+
|  线程1 请求锁      | ---> 进入等待队列
+-------------------+
+-------------------+
|  线程2 请求锁      | ---> 进入等待队列
+-------------------+
+-------------------+
|  线程3 请求锁      | ---> 进入等待队列
+-------------------+
1.2 非公平锁

非公平锁则是一种允许线程“抢占”锁的机制。当某个线程请求锁时,它可以尝试直接获取锁,而不必等待已经在等待队列中的线程。这意味着如果当前锁没有被其他线程持有,那么请求锁的线程可以直接获取到锁,而不用排队。

特点

  • 可能导致线程饥饿:某些线程可能长时间获取不到锁。
  • 性能较高,因为非公平锁减少了上下文切换和线程调度的开销。

示意图:非公平锁的锁获取流程

+-------------------+
|  线程1 请求锁      | ---> 立即尝试获取锁
+-------------------+
+-------------------+
|  线程2 请求锁      | ---> 立即尝试获取锁
+-------------------+
+-------------------+
|  线程3 请求锁      | ---> 立即尝试获取锁
+-------------------+

第二部分:公平锁的优缺点

2.1 公平锁的优点
  1. 避免线程饥饿:公平锁严格按照请求的顺序分配锁,保证每个线程都有机会获取锁,防止某些线程长时间等待,特别是在高并发场景中,这种公平机制非常重要。

  2. 任务的公平调度:在某些业务逻辑中,要求任务按照请求的顺序来处理,因此使用公平锁可以确保任务的顺序性,防止一些任务提前或被长时间延迟。

2.2 公平锁的缺点
  1. 性能开销较大:公平锁需要维护一个排队机制,每当有新线程请求锁时,系统需要检查等待队列中的线程并按照顺序分配锁,这增加了锁的获取成本。

  2. 上下文切换较频繁:由于每个线程都需要按照顺序等待获取锁,频繁的线程调度和上下文切换可能会降低系统的性能,特别是在高并发的场景中。

  3. 降低吞吐量:由于公平锁严格按照顺序分配锁,某些本可以快速执行的线程也需要等待队列中其他线程先获取锁,这可能导致系统吞吐量的降低。

示意图:公平锁中的上下文切换

+--------------------+               +--------------------+
| 线程1 获取锁         |  --> 切换到   | 线程2 等待获取锁     |
+--------------------+               +--------------------+
+--------------------+               +--------------------+
| 线程2 获取锁         |  --> 切换到   | 线程3 等待获取锁     |
+--------------------+               +--------------------+

第三部分:非公平锁的优缺点

3.1 非公平锁的优点
  1. 高性能:非公平锁允许线程“抢占”锁,而不必按照排队的顺序等待。这样可以减少线程的上下文切换和调度开销,从而提升系统的并发性能。

  2. 高吞吐量:由于锁可以被直接抢占,非公平锁在某些情况下能够快速处理短时间的任务,提高系统的整体吞吐量。

3.2 非公平锁的缺点
  1. 可能导致线程饥饿:非公平锁不保证线程获取锁的顺序,某些线程可能长期处于等待状态,特别是在高并发场景中,如果线程抢不到锁,就会出现线程饥饿问题。

  2. 任务顺序不确定:非公平锁不保证任务的执行顺序,在某些对顺序要求严格的场景下,非公平锁可能不合适。


第四部分:ReentrantLock 的公平锁和非公平锁实现

4.1 ReentrantLock 概述

ReentrantLock 是 Java 提供的显式锁,它比 synchronized 具有更丰富的功能。ReentrantLock 允许线程重复获取锁,并提供了公平锁与非公平锁的选择。

  • 公平锁ReentrantLock 的构造方法可以通过传入 true 参数来创建公平锁。
  • 非公平锁ReentrantLock 的默认构造方法使用非公平锁,也可以通过传入 false 参数来创建非公平锁。
4.2 ReentrantLock 的公平锁实现

当使用公平锁时,ReentrantLock 通过内部维护一个FIFO队列来确保每个线程按照请求顺序获取锁。具体实现上,ReentrantLock 使用 AbstractQueuedSynchronizer(AQS)的 tryAcquire 方法,检查当前是否有其他线程在排队,确保锁按照顺序分配。

代码示例:ReentrantLock 公平锁

import java.util.concurrent.locks.ReentrantLock;

public class FairLockExample {
    private static final ReentrantLock lock = new ReentrantLock(true);  // 公平锁

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new Task(), "线程-" + i).start();
        }
    }

    static class Task implements Runnable {
        @Override
        public void run() {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " 获取了锁");
                Thread.sleep(1000);  // 模拟业务操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

运行结果(不同线程获取锁的顺序依次进行):

线程-0 获取了锁
线程-1 获取了锁
线程-2 获取了锁
线程-3 获取了锁
线程-4 获取了锁

实现细节

  1. 当线程尝试获取锁时,ReentrantLock 会检查是否有其他线程在等待队列中。如果有,当前线程会被加入到等待队列的末尾。
  2. 锁的释放时,系统会唤醒等待队列中的第一个线程,从而保证锁的获取顺序是公平的。
4.3 ReentrantLock 的非公平锁实现

非公平锁是 ReentrantLock 的默认锁实现。在非公平锁中,线程在请求锁时,不会关心等待队列中的顺序,而是直接尝试获取锁。这种抢占式的策略减少了排队和上下文切换,提高了系统的性能。

代码示例:ReentrantLock 非公平锁

import java.util.concurrent.locks.ReentrantLock;

public class UnfairLockExample {
    private static final ReentrantLock lock = new ReentrantLock();  // 非公平锁

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new Task(), "线程-" + i).start();
        }
    }

    static class Task implements Runnable {
        @Override
        public void run() {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " 获取了锁");
                Thread.sleep(

1000);  // 模拟业务操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

运行结果(不同线程获取锁的顺序可能不按提交顺序进行):

线程-0 获取了锁
线程-1 获取了锁
线程-3 获取了锁
线程-2 获取了锁
线程-4 获取了锁

实现细节

  1. 线程直接尝试获取锁,如果锁是可用的,立即获取,不需要查看等待队列中的其他线程。
  2. 由于线程不按顺序排队,某些线程可能抢占其他线程的锁,从而提升了性能,但可能导致某些线程长期等待。
4.4 公平锁与非公平锁的性能比较

公平锁和非公平锁的核心区别在于锁的分配顺序上。公平锁保证了锁的顺序性,而非公平锁则允许锁的抢占。因此,它们在不同的应用场景下表现不同:

  • 公平锁:适合对顺序性要求高的场景,但由于频繁的上下文切换,性能可能较低。
  • 非公平锁:适合高并发、低延迟的场景,能够提升系统的吞吐量,但可能导致某些线程长期处于饥饿状态。

第五部分:ReentrantLock 内部如何实现公平锁与非公平锁

ReentrantLock 的内部是通过 AbstractQueuedSynchronizer(AQS)来管理锁的状态和线程队列。AQS 提供了 FIFO 队列来管理线程的等待,公平锁与非公平锁的实现主要区别在于 tryAcquire() 方法。

5.1 AQS 中的公平锁实现

对于公平锁,ReentrantLock 会首先检查等待队列中是否有其他线程,如果有,当前线程会被加入队列,等待前面的线程获取和释放锁。

代码示例:AQS 中的公平锁实现

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    
    // 如果当前锁是空闲的,且没有其他线程在等待,则当前线程获取锁
    if (c == 0) {
        if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    return false;
}
5.2 AQS 中的非公平锁实现

对于非公平锁,线程在获取锁时,不会检查等待队列中的其他线程,而是直接尝试获取锁。

代码示例:AQS 中的非公平锁实现

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    
    // 非公平锁直接尝试获取锁
    if (c == 0 && compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
    }
    return false;
}
5.3 公平锁与非公平锁的选择

ReentrantLock 提供了两种构造方法,开发者可以选择使用公平锁或非公平锁:

// 使用公平锁
ReentrantLock fairLock = new ReentrantLock(true);

// 使用非公平锁
ReentrantLock unfairLock = new ReentrantLock(false);

第六部分:公平锁与非公平锁的应用场景

6.1 公平锁的应用场景

公平锁适用于对锁的顺序性要求较高的场景,典型应用包括:

  • 银行排队系统:保证客户按顺序办理业务。
  • 任务调度系统:确保任务按照提交的顺序执行,避免任务的无序执行导致结果错误。
6.2 非公平锁的应用场景

非公平锁适用于高并发、对性能要求较高的场景,典型应用包括:

  • Web服务器:在处理高并发请求时,非公平锁能够减少锁竞争带来的性能损耗。
  • 数据库连接池:非公平锁可以提高数据库连接的利用率,减少线程切换带来的开销。

第七部分:公平锁与非公平锁的性能测试

通过实际的性能测试,可以更直观地了解公平锁与非公平锁在不同并发场景下的表现。以下是一个性能测试的示例代码,用于对比公平锁与非公平锁在高并发场景下的表现。

代码示例:公平锁与非公平锁的性能测试

import java.util.concurrent.locks.ReentrantLock;

public class LockPerformanceTest {
    private static final int THREAD_COUNT = 100;
    private static final ReentrantLock fairLock = new ReentrantLock(true);  // 公平锁
    private static final ReentrantLock unfairLock = new ReentrantLock(false);  // 非公平锁

    public static void main(String[] args) throws InterruptedException {
        testLockPerformance(fairLock, "公平锁");
        testLockPerformance(unfairLock, "非公平锁");
    }

    private static void testLockPerformance(ReentrantLock lock, String lockType) throws InterruptedException {
        long startTime = System.currentTimeMillis();

        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 100000; j++) {
                    lock.lock();
                    try {
                        // 模拟业务操作
                    } finally {
                        lock.unlock();
                    }
                }
            });
        }

        // 启动所有线程
        for (Thread thread : threads) {
            thread.start();
        }

        // 等待所有线程完成
        for (Thread thread : threads) {
            thread.join();
        }

        long endTime = System.currentTimeMillis();
        System.out.println(lockType + " 耗时: " + (endTime - startTime) + " 毫秒");
    }
}

运行结果

公平锁 耗时: 3500 毫秒
非公平锁 耗时: 2900 毫秒

分析

  • 在高并发场景下,非公平锁由于减少了线程的上下文切换,性能更高。
  • 公平锁由于需要维护线程的顺序性,性能相对较低,但保证了线程的公平性。

第八部分:总结

公平锁与非公平锁是并发编程中两种常见的锁机制,它们各自具有不同的优缺点,适用于不同的应用场景。在高并发的环境下,非公平锁能够带来更高的性能,但可能会导致某些线程饥饿。而公平锁则通过维护一个FIFO队列,保证线程能够按照请求顺序获取锁,防止线程饥饿,但会带来一定的性能损耗。

ReentrantLock 提供了对公平锁与非公平锁的支持,开发者可以根据实际需求选择合适的锁类型。在需要保证任务顺序性的场景下,公平锁是较好的选择;而在对性能要求较高的场景中,非公平锁更具优势。

通过对这两种锁的实现原理、性能对比以及应用场景的分析,开发者可以更好地理解并选择适合自己的锁机制,从而提高系统的并发性能和稳定性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CopyLower

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值