JAVA ReentrantLock公平锁与非公平锁性能差异实测与选择指南

JAVA ReentrantLock 公平锁与非公平锁:性能实测与选择指南

各位来宾,大家好!今天我们来深入探讨 Java 并发编程中一个重要的工具:ReentrantLock。更具体地说,我们将聚焦于 ReentrantLock 的公平锁与非公平锁,通过实际测试来分析它们的性能差异,并提供在不同场景下选择的指导。

1. ReentrantLock 基础与公平性概念

ReentrantLockjava.util.concurrent.locks 包中的一个可重入互斥锁,它提供了比 synchronized 关键字更强大的功能。 它可以实现公平锁和非公平锁两种策略。

  • 可重入性: 允许同一个线程多次获取同一个锁,而不会被阻塞。这对于递归调用等场景非常重要。
  • 互斥性: 保证同一时刻只有一个线程持有锁,从而保护共享资源免受并发访问的破坏。

ReentrantLock 的公平性指的是锁的获取顺序。

  • 公平锁: 按照线程请求锁的顺序来分配锁。如果一个线程已经等待了很长时间,它更有可能获得锁。这保证了所有线程都有机会获得锁,避免了饥饿现象。
  • 非公平锁: 允许线程“插队”,即如果锁当前空闲,即使有其他线程在等待,当前请求的线程也可能直接获得锁。这通常可以提高吞吐量,但可能导致某些线程长时间无法获得锁。

2. ReentrantLock 的使用

ReentrantLock 的基本用法如下:

import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock(); // 默认是非公平锁
    //private final ReentrantLock lock = new ReentrantLock(true); // 创建公平锁

    public void increment() {
        lock.lock(); // 获取锁
        try {
            count++;
        } finally {
            lock.unlock(); // 释放锁,必须在 finally 块中释放
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

在上面的代码中,lock.lock() 方法尝试获取锁。如果锁当前被其他线程持有,当前线程将被阻塞,直到锁被释放。lock.unlock() 方法释放锁,允许其他线程获取锁。务必将 lock.unlock() 放在 finally 块中,以确保即使在发生异常的情况下也能正确释放锁,避免死锁。

可以通过构造函数 ReentrantLock(boolean fair) 来指定锁的公平性。fair 参数为 true 时创建公平锁,为 false 时创建非公平锁(默认值)。

3. 公平锁与非公平锁的性能测试

为了测试公平锁和非公平锁的性能差异,我们需要设计一个并发场景。我们将创建一个计数器,多个线程并发地对其进行递增操作,并分别使用公平锁和非公平锁来保护计数器。

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class LockPerformanceTest {

    private static final int NUM_THREADS = 10;
    private static final int NUM_INCREMENTS = 1000000;

    static class Counter {
        private int count = 0;
        private final ReentrantLock lock;

        public Counter(boolean fair) {
            this.lock = new ReentrantLock(fair);
        }

        public void increment() {
            lock.lock();
            try {
                count++;
            } finally {
                lock.unlock();
            }
        }

        public int getCount() {
            return count;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("Running Fair Lock Test...");
        testLockPerformance(true);

        System.out.println("nRunning Non-Fair Lock Test...");
        testLockPerformance(false);
    }

    private static void testLockPerformance(boolean fair) throws InterruptedException {
        Counter counter = new Counter(fair);
        ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);
        AtomicInteger completionCounter = new AtomicInteger(0); // track thread completion

        long startTime = System.nanoTime();

        for (int i = 0; i < NUM_THREADS; i++) {
            executor.submit(() -> {
                for (int j = 0; j < NUM_INCREMENTS; j++) {
                    counter.increment();
                }
                completionCounter.incrementAndGet();
            });
        }

        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);

        long endTime = System.nanoTime();
        long duration = (endTime - startTime) / 1000000; // milliseconds

        System.out.println("Fairness: " + fair);
        System.out.println("Total Count: " + counter.getCount());
        System.out.println("Time taken: " + duration + " ms");
        System.out.println("Average time per increment: " + (double) duration / (NUM_THREADS * NUM_INCREMENTS) * 1000 + " microseconds");
    }
}

在这个测试中:

  • 我们创建了一个 Counter 类,它使用 ReentrantLock 来保护 count 变量。
  • NUM_THREADS 定义了并发线程的数量,NUM_INCREMENTS 定义了每个线程递增 count 的次数。
  • testLockPerformance 方法分别使用公平锁和非公平锁来运行测试,并测量执行时间。
  • 使用 AtomicInteger 来准确的判断所有线程完成。

测试结果分析

运行上述代码,我们可以得到公平锁和非公平锁的性能数据。通常情况下,你会发现:

  • 非公平锁的吞吐量更高: 非公平锁允许线程插队,减少了线程上下文切换的开销,从而提高了吞吐量。
  • 公平锁的公平性更好: 公平锁保证了所有线程都有机会获得锁,避免了饥饿现象。但为了维护公平性,它需要额外的开销,例如维护一个等待队列,这会降低吞吐量。

以下是一个典型的测试结果示例(结果可能会因硬件和 JVM 配置而异):

特性 公平锁 非公平锁
总计数 10000000 10000000
执行时间 (ms) 1200 800
平均每次递增时间 (微秒) 0.12 0.08

从这个示例可以看出,非公平锁的执行时间更短,平均每次递增的时间也更少,这表明它的吞吐量更高。

4. 深入理解性能差异的原因

  • 上下文切换: 公平锁需要维护一个等待队列,并按照队列的顺序唤醒线程。这涉及到更多的线程上下文切换,而上下文切换是一项昂贵的操作。非公平锁则允许线程直接尝试获取锁,减少了上下文切换的次数。
  • 调度开销: 公平锁需要额外的调度开销来保证公平性。非公平锁则没有这个开销,它可以更快速地分配锁。
  • CPU 竞争: 在高并发情况下,非公平锁可能导致某些线程长时间无法获得锁,造成饥饿现象。但这通常只发生在非常激烈的竞争环境中。

5. 选择指南:如何选择公平锁或非公平锁

在选择公平锁或非公平锁时,需要权衡吞吐量和公平性。

  • 如果吞吐量是首要考虑因素: 优先选择非公平锁。大多数情况下,非公平锁可以提供更好的性能。例如,在高并发的 Web 服务器中,吞吐量至关重要,非公平锁是一个不错的选择。

  • 如果公平性是首要考虑因素: 选择公平锁。如果需要保证所有线程都有机会获得锁,避免饥饿现象,那么公平锁是更好的选择。例如,在某些实时系统中,需要保证所有任务都能及时得到处理,公平锁可以避免某些任务被长时间阻塞。

  • 实际情况的考量: 在实际应用中,还需要考虑具体的并发场景。如果并发量不高,或者锁的持有时间很短,那么公平锁和非公平锁的性能差异可能并不明显。在这种情况下,可以根据实际需求选择。

    • 低并发场景: 非公平锁,因为开销更小
    • 高并发,但资源竞争不激烈场景: 非公平锁,吞吐量更重要
    • 高并发,资源竞争激烈,且需要避免饥饿: 公平锁

6. 避免死锁

无论选择公平锁还是非公平锁,都需要注意避免死锁。死锁是指两个或多个线程互相等待对方释放锁,导致所有线程都无法继续执行的情况。

以下是一些避免死锁的常见方法:

  • 避免循环等待: 确保线程获取锁的顺序是固定的,避免出现循环等待的情况。
  • 设置超时时间: 在尝试获取锁时,设置一个超时时间。如果超过超时时间仍未获得锁,则放弃获取锁,避免无限等待。
  • 使用 tryLock() 方法: ReentrantLock 提供了 tryLock() 方法,它尝试获取锁,如果锁当前被其他线程持有,则立即返回 false,而不是阻塞等待。可以使用 tryLock() 方法来避免死锁。

7. 其他 ReentrantLock 的高级特性

除了公平性之外,ReentrantLock 还提供了其他一些高级特性:

  • 可中断锁: 允许线程在等待锁的过程中被中断。这可以通过 lockInterruptibly() 方法来实现。
  • 条件变量: ReentrantLock 提供了条件变量(Condition),可以用来实现线程间的协作。条件变量允许线程在满足特定条件时暂停执行,并在其他线程满足条件时被唤醒。

8. 代码示例:使用 tryLock() 避免死锁

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class DeadlockAvoidance {

    private final ReentrantLock lock1 = new ReentrantLock();
    private final ReentrantLock lock2 = new ReentrantLock();

    public void method1() {
        try {
            if (lock1.tryLock(1, TimeUnit.SECONDS)) {
                try {
                    System.out.println("Method 1 acquired lock1");
                    Thread.sleep(100); // Simulate some work
                    if (lock2.tryLock(1, TimeUnit.SECONDS)) {
                        try {
                            System.out.println("Method 1 acquired lock2");
                            // Do some work with both locks held
                        } finally {
                            lock2.unlock();
                            System.out.println("Method 1 released lock2");
                        }
                    } else {
                        System.out.println("Method 1 failed to acquire lock2, releasing lock1");
                    }
                } finally {
                    lock1.unlock();
                    System.out.println("Method 1 released lock1");
                }
            } else {
                System.out.println("Method 1 failed to acquire lock1");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void method2() {
        try {
            if (lock2.tryLock(1, TimeUnit.SECONDS)) {
                try {
                    System.out.println("Method 2 acquired lock2");
                    Thread.sleep(100); // Simulate some work
                    if (lock1.tryLock(1, TimeUnit.SECONDS)) {
                        try {
                            System.out.println("Method 2 acquired lock1");
                            // Do some work with both locks held
                        } finally {
                            lock1.unlock();
                            System.out.println("Method 2 released lock1");
                        }
                    } else {
                        System.out.println("Method 2 failed to acquire lock1, releasing lock2");
                    }
                } finally {
                    lock2.unlock();
                    System.out.println("Method 2 released lock2");
                }
            } else {
                System.out.println("Method 2 failed to acquire lock2");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        DeadlockAvoidance deadlockAvoidance = new DeadlockAvoidance();

        Thread thread1 = new Thread(deadlockAvoidance::method1);
        Thread thread2 = new Thread(deadlockAvoidance::method2);

        thread1.start();
        thread2.start();
    }
}

在这个例子中,method1 尝试先获取 lock1,然后获取 lock2,而 method2 尝试先获取 lock2,然后获取 lock1。这可能导致死锁。通过使用 tryLock(1, TimeUnit.SECONDS) 方法,我们可以设置一个超时时间,如果线程在指定时间内无法获得锁,则放弃获取锁,从而避免死锁。

9. 代码示例:使用 Condition 实现线程协作

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {

    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private boolean isDataReady = false;

    public void produceData() {
        lock.lock();
        try {
            while (isDataReady) {
                try {
                    condition.await(); // Wait until data is consumed
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // Produce data
            System.out.println("Producing data...");
            isDataReady = true;
            condition.signal(); // Signal that data is ready
        } finally {
            lock.unlock();
        }
    }

    public void consumeData() {
        lock.lock();
        try {
            while (!isDataReady) {
                try {
                    condition.await(); // Wait until data is produced
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // Consume data
            System.out.println("Consuming data...");
            isDataReady = false;
            condition.signal(); // Signal that data is consumed
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ConditionExample conditionExample = new ConditionExample();

        Thread producer = new Thread(conditionExample::produceData);
        Thread consumer = new Thread(conditionExample::consumeData);

        producer.start();
        consumer.start();
    }
}

在这个例子中,produceData 方法生产数据,consumeData 方法消费数据。通过使用条件变量 condition,我们可以实现生产者和消费者之间的协作。await() 方法使线程进入等待状态,直到其他线程调用 signal()signalAll() 方法唤醒它。signal() 方法唤醒等待队列中的一个线程,而 signalAll() 方法唤醒等待队列中的所有线程。

10. 总结与建议

今天的讲座,我们深入探讨了 Java ReentrantLock 的公平锁与非公平锁。非公平锁通常提供更高的吞吐量,而公平锁则保证了公平性,避免了饥饿现象。在选择时,需要根据具体的并发场景权衡吞吐量和公平性,并在实际应用中灵活运用 tryLock() 和条件变量等高级特性,以确保程序的正确性和性能。希望今天的讲解能够帮助大家更好地理解和使用 ReentrantLock

性能与公平,权衡决定选择

非公平锁性能更佳,公平锁避免饥饿。
选择策略需根据实际场景,权衡吞吐与公平。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注