Java ReentrantLock详解与应用实战

ReentrantLock是Java并发包(java.util.concurrent.locks)中提供的一种可重入互斥锁,它作为synchronized关键字的替代方案,提供了更灵活、更强大的线程同步机制。本文将全面解析ReentrantLock的核心特性、实现原理及实际应用场景。

ReentrantLock概述与基本特性

ReentrantLock是Java 5引入的显式锁机制,它基于AQS(AbstractQueuedSynchronizer)框架实现,提供了比synchronized更丰富的功能和控制能力。与synchronized相比,ReentrantLock具有以下显著特点:

  • 可重入性:同一线程可以多次获得同一把锁而不会被阻塞,每次获取锁后必须释放相同次数的锁
  • 公平性选择:支持公平锁和非公平锁两种策略,公平锁按照线程请求顺序分配锁,非公平锁允许"插队"以提高吞吐量
  • 灵活的锁获取方式:提供尝试非阻塞获取锁(tryLock)、可中断获取锁(lockInterruptibly)和超时获取锁(tryLock with timeout)等方法
  • 条件变量支持:通过Condition接口实现多个等待队列,比synchronized的wait/notify机制更精准

从实现层级看,synchronized是JVM内置的锁机制,通过monitorenter/monitorexit字节码指令实现;而ReentrantLock是JDK API级别的锁,基于AQS框架构建。

ReentrantLock核心方法与使用

基础锁操作

ReentrantLock的基本使用模式遵循"加锁-操作-释放锁"的流程,必须确保在finally块中释放锁:

csharp 复制代码
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区代码
} finally {
    lock.unlock();
}

这种显式锁管理相比synchronized需要更多注意,但提供了更精细的控制。

高级锁获取方式

  1. 尝试非阻塞获取锁(tryLock)​​:

    立即返回获取结果,不阻塞线程,适用于避免死锁或快速失败场景:

    csharp 复制代码
    if (lock.tryLock()) {
        try {
            // 临界区代码
        } finally {
            lock.unlock();
        }
    } else {
        // 执行备选方案
    }
  2. 超时获取锁​:

    在指定时间内尝试获取锁,避免无限期等待:

    csharp 复制代码
    if (lock.tryLock(2, TimeUnit.SECONDS)) {
        try {
            // 临界区代码
        } finally {
            lock.unlock();
        }
    }
  3. 可中断获取锁(lockInterruptibly)​​:

    允许在等待锁的过程中响应中断信号:

    csharp 复制代码
    try {
        lock.lockInterruptibly();
        try {
            // 临界区代码
        } finally {
            lock.unlock();
        }
    } catch (InterruptedException e) {
        // 处理中断
    }

锁状态查询

ReentrantLock提供了一系列状态查询方法:

  • isLocked():查询锁是否被持有
  • isHeldByCurrentThread():当前线程是否持有锁
  • getHoldCount():当前线程持有锁的次数(重入次数)
  • getQueueLength():等待获取锁的线程数

ReentrantLock实现原理

AQS框架基础

ReentrantLock的核心实现依赖于AbstractQueuedSynchronizer(AQS),这是一个用于构建锁和同步器的框架。AQS内部维护了:

  • volatile int state:同步状态,对于ReentrantLock,0表示未锁定,>0表示锁定状态及重入次数
  • FIFO线程等待队列:管理获取锁失败的线程

公平锁与非公平锁实现

ReentrantLock通过两种不同的Sync子类实现锁策略:

  1. 非公平锁(默认)​​:

    scss 复制代码
    final void lock() {
        if (compareAndSetState(0, 1))  // 直接尝试抢占
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    新请求的线程可以直接插队尝试获取锁,不考虑等待队列

  2. 公平锁​:

    arduino 复制代码
    protected final boolean tryAcquire(int acquires) {
        if (!hasQueuedPredecessors() &&  // 检查是否有前驱节点
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        // 可重入逻辑...
    }

    严格按照FIFO顺序分配锁,避免饥饿现象

锁的获取与释放流程

  1. 加锁过程​:

    • 尝试通过CAS修改state状态
    • 成功则设置当前线程为独占线程
    • 失败则构造Node加入CLH队列尾部,并阻塞线程
  2. 释放过程​:

    • 减少持有计数(state减1)
    • 当state为0时完全释放锁
    • 唤醒队列中的下一个等待线程

ReentrantLock实战应用

生产者-消费者模型

使用ReentrantLock配合Condition实现高效的生产者-消费者模式:

ini 复制代码
public class BoundedBuffer {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    private final Object[] items = new Object[100];
    private int putPtr, takePtr, count;

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();  // 等待"不满"条件
            items[putPtr] = x;
            if (++putPtr == items.length) putPtr = 0;
            ++count;
            notEmpty.signal();  // 通知"不空"条件
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();  // 等待"不空"条件
            Object x = items[takePtr];
            if (++takePtr == items.length) takePtr = 0;
            --count;
            notFull.signal();  // 通知"不满"条件
            return x;
        } finally {
            lock.unlock();
        }
    }
}

这种实现比synchronized+wait/notify更高效,因为可以精准唤醒生产者或消费者线程。

银行转账避免死锁

使用tryLock实现带超时的转账操作,避免死锁:

vbnet 复制代码
public boolean transfer(Account from, Account to, int amount, long timeout, TimeUnit unit) {
    long stopTime = System.nanoTime() + unit.toNanos(timeout);
    while (true) {
        if (from.getLock().tryLock()) {
            try {
                if (to.getLock().tryLock()) {
                    try {
                        if (from.getBalance() < amount)
                            throw new InsufficientFundsException();
                        from.withdraw(amount);
                        to.deposit(amount);
                        return true;
                    } finally {
                        to.getLock().unlock();
                    }
                }
            } finally {
                from.getLock().unlock();
            }
        }
        if (System.nanoTime() > stopTime)
            return false;
        Thread.sleep(fixedDelay);
    }
}

通过tryLock和超时机制,有效预防了死锁风险。

可中断的任务执行

使用lockInterruptibly实现可中断的任务执行:

csharp 复制代码
public class InterruptibleTask {
    private final ReentrantLock lock = new ReentrantLock();
    
    public void executeTask() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            // 执行可能长时间运行的任务
            while (!Thread.currentThread().isInterrupted()) {
                // 任务逻辑...
            }
        } finally {
            lock.unlock();
        }
    }
}

这种模式适用于需要支持任务取消的场景。

ReentrantLock与synchronized的对比

特性 synchronized ReentrantLock
实现层级 JVM内置 JDK API实现
锁释放 自动 必须手动调用unlock()
公平锁支持 仅非公平 支持公平和非公平策略
可中断获取锁 不支持 支持(lockInterruptibly)
超时获取锁 不支持 支持(tryLock with timeout)
条件变量 单一等待队列 支持多个Condition
锁状态查询 有限 提供丰富查询方法
性能 Java 6+优化 复杂场景下表现更好
代码简洁性 较低(需手动管理)
适用场景 简单同步 复杂同步需求

在Java 6及以后版本中,synchronized经过锁升级(偏向锁→轻量级锁→重量级锁)优化,性能与ReentrantLock差距已不明显。因此,简单场景推荐使用synchronized,复杂场景才考虑ReentrantLock。

ReentrantLock最佳实践

  1. 始终在finally块中释放锁​:

    确保锁一定会被释放,避免死锁:

    csharp 复制代码
    lock.lock();
    try {
        // 临界区代码
    } finally {
        lock.unlock();
    }
  2. 避免嵌套锁​:

    尽量不要在持有一个锁的情况下尝试获取另一个锁,容易导致死锁。

  3. 合理选择锁策略​:

    • 高吞吐场景:非公平锁(默认)
    • 避免饥饿场景:公平锁
  4. 优先使用tryLock​:

    特别是涉及多个锁的操作,使用tryLock可以避免死锁。

  5. 合理使用Condition​:

    替代Object的wait/notify,实现更精准的线程通信。

  6. 性能考量​:

    简单同步场景优先选择synchronized,复杂场景才使用ReentrantLock。

总结

ReentrantLock作为Java并发编程中的重要工具,通过其可重入性、公平性选择、灵活的锁获取方式和条件变量支持,为开发者提供了比synchronized更强大的线程同步能力。理解其基于AQS的实现原理,掌握各种高级特性的使用方法,并遵循最佳实践,可以帮助我们构建更高效、更健壮的并发程序。在实际开发中,应根据具体场景需求,在synchronized和ReentrantLock之间做出合理选择。

相关推荐
间彧2 小时前
volatile与Atomic类的性能对比与适用场景分析
java
间彧3 小时前
Java Atomic类详解与实战应用
java
间彧3 小时前
Java 中volatile详解与应用
java
多多*3 小时前
2025最新centos7安装mysql8 相关 服务器配置 纯命令行操作 保姆级教程
java·运维·服务器·mysql·spring·adb
寻星探路3 小时前
Java EE初阶启程记03---Thread类及常见方法
java·java-ee
计算机学姐3 小时前
基于微信小程序的智能在线预约挂号系统【2026最新】
java·vue.js·spring boot·mysql·微信小程序·小程序·tomcat
m0_749317523 小时前
apifox认证登录自动化
java·学习·测试工具·自动化
cici158743 小时前
在Ubuntu18.04安装兼容JDK 8的Eclipse集成开发环境
java·开发语言·eclipse
老赵的博客3 小时前
c++ 之多态虚函数表
java·jvm·c++