杰瑞科技汇

ReentrantLock在Java中如何正确使用与优化?

ReentrantLock 深度解析:告别 synchronized,解锁 Java 并发编程新境界

在 Java 并发编程的世界里,synchronized 关键词几乎是所有开发者的启蒙,随着业务复杂度的提升,synchronized 的局限性也逐渐显现。java.util.concurrent.locks.ReentrantLock 作为 synchronized 的有力补充,提供了更强大、更灵活的锁机制,本文将从基础到进阶,全方位剖析 ReentrantLock 的核心原理、使用方法、与 synchronized 的对比,以及在实际开发中的最佳实践,助你彻底掌握这一并发利器。

ReentrantLock在Java中如何正确使用与优化?-图1
(图片来源网络,侵删)

开篇:为什么我们需要 ReentrantLock?

想象一个场景:你正在开发一个高并发的秒杀系统,需要对库存进行扣减,使用 synchronized 关键字,代码可能是这样的:

public class SynchronizedExample {
    private int stock = 100;
    public synchronized void decrement() {
        if (stock > 0) {
            stock--;
            System.out.println(Thread.currentThread().getName() + " 扣减成功,剩余库存: " + stock);
        } else {
            System.out.println(Thread.currentThread().getName() + " 扣减失败,库存不足");
        }
    }
}

这段代码简单有效,但很快你就会遇到新的需求:

  1. 尝试获取锁:如果锁被其他线程占用,我不想一直等待,而是想立即返回“失败”状态,去做其他事情。
  2. 公平锁:我希望锁能分配给等待时间最长的线程,而不是随机分配,避免“饥饿”现象。
  3. 可中断地获取锁:如果线程在等待锁的过程中,我想通过中断(interrupt())的方式让它放弃等待。
  4. 获取锁的超时控制:我只想等待一定的时间,500ms,如果还没拿到锁,就放弃。

synchronized 关键字无法满足这些精细化的控制需求,这时,ReentrantLock 便闪亮登场,它就像一个功能强大的“瑞士军刀”,为解决复杂的并发问题提供了可能。


ReentrantLock 是什么?

ReentrantLockjava.util.concurrent.locks 包下的一个类,它实现了 Lock 接口,从名字可以看出,它是一个可重入锁

ReentrantLock在Java中如何正确使用与优化?-图2
(图片来源网络,侵删)

可重入(Reentrant) 的意思是,一个已经获取了锁的线程,可以再次获取同一个锁而不会死锁,这和 synchronized 的行为是一致的,都是为了支持在同步代码块中调用其他同步方法。

ReentrantLock lock = new ReentrantLock();
public void methodA() {
    lock.lock();
    try {
        System.out.println("进入 methodA");
        methodB(); // 在持有锁的情况下再次获取锁
    } finally {
        lock.unlock();
    }
}
public void methodB() {
    lock.lock();
    try {
        System.out.println("进入 methodB");
    } finally {
        lock.unlock();
    }
}

线程调用 methodA() 获取锁后,再调用 methodB() 时,可以成功再次获取锁,而不会发生死锁。


ReentrantLock 的核心用法与 API

使用 ReentrantLock 的标准模式是 try-finally,以确保锁一定会被释放,避免死锁。

Lock lock = new ReentrantLock();
public void doSomething() {
    lock.lock(); // 获取锁
    try {
        // 临界区(被保护的代码)
        System.out.println("线程 " + Thread.currentThread().getName() + " 正在工作...");
    } finally {
        lock.unlock(); // 释放锁
    }
}

ReentrantLock 提供了比 synchronized 更丰富的 API,主要体现在获取锁的方式上:

ReentrantLock在Java中如何正确使用与优化?-图3
(图片来源网络,侵删)

lock() / unlock() - 基础方法

  • lock():尝试获取锁,如果锁被其他线程占用,则当前线程会一直阻塞,直到获取到锁。
  • unlock():释放锁,必须在 try-finally 块中调用,以确保异常发生时锁也能被释放。

tryLock() - 尝试获取锁

tryLock() 是 ReentrantLock 的精髓之一,它允许线程在尝试获取锁时不被阻塞

  • tryLock():尝试获取锁,如果成功,返回 true;如果锁被其他线程占用,立即返回 false,线程继续执行后续代码。
public void tryLockExample() {
    if (lock.tryLock()) { // 尝试获取锁,立即返回结果
        try {
            System.out.println("成功获取锁,执行任务");
        } finally {
            lock.unlock();
        }
    } else {
        System.out.println("获取锁失败,执行其他任务或稍后重试");
    }
}
  • tryLock(long time, TimeUnit unit):在指定的时间内尝试获取锁,如果在超时时间内获取到锁,返回 true;如果超时仍未获取,返回 false
public void tryLockWithTimeoutExample() throws InterruptedException {
    if (lock.tryLock(500, TimeUnit.MILLISECONDS)) { // 最多等待500ms
        try {
            System.out.println("在超时时间内成功获取锁");
        } finally {
            lock.unlock();
        }
    } else {
        System.out.println("等待500ms后仍未获取到锁");
    }
}

lockInterruptibly() - 可中断地获取锁

当一个线程通过 lockInterruptibly() 获取锁时,如果它在等待过程中被其他线程中断(interrupt()),它会响应中断,并抛出 InterruptedException,然后立即返回。

public void lockInterruptiblyExample() throws InterruptedException {
    lock.lockInterruptibly(); // 可以响应中断的获取锁方式
    try {
        System.out.println("成功获取锁,开始执行任务");
        // ... 长时间任务
    } finally {
        lock.unlock();
    }
}

这在需要实现“取消”或“超时”逻辑的场景中非常有用,避免了线程无限期等待。

newCondition() - 条件变量

ReentrantLock 可以关联一个或多个 Condition 对象,相当于 synchronized 内置的 wait()/notify()/notifyAll() 机制的增强版。

  • await():相当于 Object.wait(),当前线程释放锁并进入等待状态。
  • signal():相当于 Object.notify(),唤醒一个等待的线程。
  • signalAll():相当于 Object.notifyAll(),唤醒所有等待的线程。

经典示例:生产者-消费者模型

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumerExample {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private int buffer = 0;
    private final int MAX_BUFFER = 10;
    public void produce() throws InterruptedException {
        lock.lock();
        try {
            while (buffer == MAX_BUFFER) {
                System.out.println("缓冲区已满,生产者等待...");
                condition.await(); // 释放锁并等待
            }
            buffer++;
            System.out.println("生产者生产,当前库存: " + buffer);
            condition.signal(); // 唤醒消费者
        } finally {
            lock.unlock();
        }
    }
    public void consume() throws InterruptedException {
        lock.lock();
        try {
            while (buffer == 0) {
                System.out.println("缓冲区为空,消费者等待...");
                condition.await(); // 释放锁并等待
            }
            buffer--;
            System.out.println("消费者消费,当前库存: " + buffer);
            condition.signal(); // 唤醒生产者
        } finally {
            lock.unlock();
        }
    }
}

使用 Condition 可以精确地唤醒指定的线程(生产者唤醒消费者,消费者唤醒生产者),避免了 notifyAll() 带来的无效唤醒和上下文切换开销。


ReentrantLock vs. synchronized:一场巅峰对决

特性 ReentrantLock synchronized
可中断获取锁 支持 (lockInterruptibly) 不支持
尝试获取锁 支持 (tryLock()) 不支持
公平锁 支持 (构造函数参数) 不支持(非公平锁)
绑定多个条件变量 支持 (newCondition()) 不支持(仅一个内置条件)
锁的释放 必须在 finally 中手动 unlock() JVM 自动释放,异常时也能释放
底层实现 AQS (AbstractQueuedSynchronizer) JVM 实现的 monitor 机制
使用灵活性
使用便捷性 (需手动加解锁) (关键字,JVM 管理)
性能 (早期) 性能更好(尤其在高竞争下) 性能稍差
性能 (JDK 1.6+) 差距不大,JVM 对 synchronized 优化显著(偏向锁、轻量级锁) 性能已大幅提升

总结与选择建议:

  • 优先使用 synchronized:对于绝大多数简单的同步场景,synchronized 代码更简洁、不易出错,JVM 的持续优化使其性能已经非常出色,它是 Java 并发的“第一选择”。
  • 选择 ReentrantLock:当你的业务场景满足以下任何一个或多个需求时,果断选择 ReentrantLock:
    1. 需要尝试获取锁(tryLock)。
    2. 需要可中断地获取锁(lockInterruptibly)。
    3. 需要实现公平锁,避免线程饥饿。
    4. 需要精确地唤醒多个等待队列中的线程(多条件变量)。

实战案例:实现一个公平的打印服务

假设我们有一个打印任务队列,多个线程需要提交打印任务,我们希望先提交的任务先被处理(公平性),可以使用 ReentrantLock 的公平锁模式。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class FairPrinterService {
    // 使用公平锁
    private final Lock lock = new ReentrantLock(true); 
    private int currentJob = 0;
    public void submitJob(String jobName) {
        lock.lock();
        try {
            currentJob++;
            System.out.println("[" + jobName + "] 提交打印任务,任务编号: " + currentJob);
            // 模拟打印耗时
            Thread.sleep(100);
            System.out.println("[" + jobName + "] 完成打印任务: " + currentJob);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        FairPrinterService service = new FairPrinterService();
        // 创建多个线程提交任务
        for (int i = 0; i < 5; i++) {
            final String threadName = "Thread-" + i;
            new Thread(() -> service.submitJob(threadName)).start();
        }
    }
}

运行结果你会发现,任务的提交顺序和执行顺序基本一致,这体现了公平锁的特性。


高级特性与最佳实践

公平锁 vs. 非公平锁

  • 公平锁new ReentrantLock(true),线程严格按照请求锁的顺序获取锁,优点是避免了线程饥饿,缺点是牺牲了吞吐量,因为线程切换和上下文切换更频繁。
  • 非公平锁new ReentrantLock(false)(默认),当一个线程释放锁时,新来的线程会和等待队列中的线程竞争锁,优点是吞吐量高,缺点是可能导致某些线程长时间获取不到锁(饥饿)。

建议:在大多数情况下,非公平锁是更好的选择,因为更高的吞吐量通常比绝对的公平性更重要。

注意事项

  1. 死锁:和 synchronized 一样,忘记 unlock() 会导致死锁。永远在 finally 块中释放锁!
  2. 锁的粒度:尽量只锁定必要的代码段,减小临界区的范围,以提高并发性能。
  3. synchronized 混用绝对不要在一个锁对象上同时使用 synchronizedReentrantLock,这会导致不可预期的行为。

ReentrantLock 是 Java 并发包中一颗璀璨的明珠,它为 synchronized 提供了强大而灵活的补充,通过本文的深入剖析,我们了解到:

  • ReentrantLock 提供了 tryLocklockInterruptibly、公平锁和 Condition 等高级功能,解决了 synchronized 无法应对的复杂场景。
  • 在选择锁机制时,应优先考虑 synchronized 的简洁性,仅在需要其高级特性时才使用 ReentrantLock
  • 正确、合理地使用 ReentrantLock,能够显著提升我们构建高性能、高并发应用的能力。

掌握 ReentrantLock,意味着你从“会用”并发工具,向“精通”并发编程迈出了坚实的一步,希望这篇文章能成为你并发征途中的有力导航,就去你的项目中尝试使用它吧!

分享:
扫描分享到社交APP
上一篇
下一篇