ReentrantLock 深度解读

ReentrantLock 是 Java java.util.concurrent.locks 包下提供的显式锁实现,它是 JDK 1.5 引入并发包(JUC)时的核心组件之一。与内置关键字 synchronized 不同,ReentrantLock 提供了更灵活的锁获取机制、更高的可控性以及丰富的功能特性。

以下从‌核心概念、底层原理、API 详解、与 synchronized 对比、最佳实践‌五个维度进行深度解析。


1. 核心概念与特性

1.1 什么是 ReentrantLock?

ReentrantLock 是一个‌可重入的互斥锁‌(Exclusive Lock)。

  • 可重入性 (Reentrancy)‌:同一个线程可以多次获取同一把锁而不会发生死锁。内部通过计数器记录重入次数,每次 lock() 计数 +1,每次 unlock() 计数 -1,直到计数为 0 才真正释放锁。
  • 互斥性‌:同一时刻只有一个线程能持有该锁。
1.2 核心特性
  1. 公平与非公平选择‌:
    • 非公平锁(默认)‌:新来的线程可以直接尝试抢占锁,无需排队。吞吐量高,但可能导致某些线程长期饥饿。
    • 公平锁‌:线程按照请求锁的顺序(FIFO)获取锁。避免了饥饿,但上下文切换开销大,吞吐量略低。
  2. 可中断等待‌:支持 lockInterruptibly(),允许等待锁的线程响应中断信号,从而避免死锁或长时间无效等待。
  3. 超时机制‌:支持 tryLock(long timeout, TimeUnit unit),若在指定时间内未获取到锁则返回 false,防止无限阻塞。
  4. 多条件变量 (Condition)‌:相比 synchronized 单一的 wait/notifyReentrantLock 可以绑定多个 Condition 对象,实现精准的线程唤醒(如:分别唤醒“读线程”和“写线程”)。

2. 底层实现原理:AQS

ReentrantLock 的核心基于 ‌AQS (AbstractQueuedSynchronizer)‌ 框架实现。

2.1 类结构
public class ReentrantLock implements Lock {
    // 内部同步器,继承自 AQS
    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {
        // ...
    }
    
    // 非公平锁实现
    static final class NonfairSync extends Sync { ... }
    
    // 公平锁实现
    static final class FairSync extends Sync { ... }
}
2.2 AQS 核心状态管理

AQS 使用一个 volatile int state 变量来表示同步状态:

  • state = 0:锁空闲。
  • state > 0:锁被持有,数值代表重入次数。
  • exclusiveOwnerThread:记录当前持有锁的线程。
2.3 加锁流程 (以非公平锁为例)
  1. CAS 尝试快速加锁‌:
    • 检查 state 是否为 0。
    • 如果是 0,通过 CAS (compareAndSetState) 将 state 设为 1,并设置当前线程为持有者。成功则返回。
    • 这是非公平锁“插队”的关键步骤。
  2. 重入判断‌:
    • 如果 state != 0,但当前线程就是持有者,则 state++,返回成功。
  3. 进入队列等待‌:
    • 如果上述两步都失败,调用 acquire(1)
    • 将当前线程封装成 Node 节点,加入 AQS 的 ‌CLH 双向队列‌尾部。
    • 线程挂起(Park),等待前驱节点释放锁后唤醒。
2.4 解锁流程
  1. 检查当前线程是否持有锁。
  2. state--
  3. 如果 state == 0,清除持有者标记,并唤醒队列中第一个等待节点(Head 的后继节点)。
  4. 如果 state > 0,说明是重入锁,仅减少计数,不唤醒其他线程。

3. API 详解与使用场景

ReentrantLock 提供了四种主要的加锁方式,适用于不同场景:

表格

方法行为描述适用场景
lock()阻塞获取锁。若锁不可用,当前线程休眠直到获取锁。‌不可中断‌。大多数常规业务场景,确保最终能执行临界区代码。
tryLock()非阻塞尝试获取锁。立即返回 true (成功) 或 false (失败)。‌不等待‌。乐观锁场景、避免死锁、轮询检查资源。即使配置了公平锁,此方法也是非公平的(只要锁空闲就抢)。
tryLock(long time, TimeUnit unit)限时等待。在指定时间内尝试获取锁,超时返回 false。‌可中断‌。对响应时间有要求的场景,防止线程无限期等待导致系统雪崩。
lockInterruptibly()阻塞获取锁,但‌响应中断‌。若线程在等待期间被中断,抛出 InterruptedException需要灵活控制线程生命周期的场景,如取消长时间运行的任务。
代码示例

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

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

    public void businessMethod() {
        // 1. 加锁
        lock.lock(); 
        try {
            // 2. 临界区业务逻辑
            System.out.println("Thread " + Thread.currentThread().getName() + " is working...");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 恢复中断状态
        } finally {
            // 3. 务必在 finally 中释放锁,防止死锁
            lock.unlock();
        }
    }

    public void tryLockMethod() {
        // 尝试获取锁,最多等待 2 秒
        try {
            if (lock.tryLock(2, TimeUnit.SECONDS)) {
                try {
                    // 业务逻辑
                } finally {
                    lock.unlock();
                }
            } else {
                // 获取锁失败,执行降级逻辑或重试
                System.out.println("Failed to acquire lock, busy.");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

 


4. ReentrantLock vs Synchronized

虽然 JDK 1.6 之后 synchronized 经过偏向锁、轻量级锁等优化,性能大幅提升,但在特定场景下 ReentrantLock 仍具有不可替代的优势。

表格

维度synchronizedReentrantLock
实现层级JVM 层面 (C++),通过 monitorenter/monitorexit 指令。JDK 层面 (Java),基于 AQS 框架。
锁释放自动释放(代码块结束或异常抛出时)。必须手动释放‌,需在 finally 块中调用 unlock()
公平性仅支持‌非公平锁‌。支持‌公平锁‌和‌非公平锁‌(构造时指定)。
中断响应等待锁时‌不可中断‌。支持 lockInterruptibly(),可响应中断。
超时控制不支持超时获取。支持 tryLock(timeout),可设置超时时间。
条件等待配合 wait()/notify(),只有一个等待队列,唤醒随机或全部。配合 Condition,可创建‌多个等待队列‌,实现精准唤醒(如 signalAll() 针对特定条件)。
性能低竞争下性能优异,高竞争下由于自旋和重量级锁切换,开销较大。高竞争下通常表现更稳定,因为可以通过自旋+队列机制减少上下文切换。
调试监控难以查看锁状态。提供 getHoldCount()isLocked()getQueueLength() 等方法,便于监控和诊断。

选型建议:

  • 优先使用 synchronized‌:代码简洁,不易出错(自动释放),适用于大多数简单的同步场景。
  • 使用 ReentrantLock‌:
    • 需要公平锁策略。
    • 需要尝试获取锁(tryLock)或超时机制。
    • 需要在等待锁时响应中断。
    • 需要多个条件变量进行精细化的线程通信。
    • 高并发竞争场景下需要更稳定的性能表现。

5. 注意事项与最佳实践

  1. 必须在 Finally 中解锁‌:
    ReentrantLock 不会自动释放锁。如果业务逻辑抛出异常且未在 finally 中解锁,其他所有等待该锁的线程将永久阻塞,导致系统瘫痪。

    lock.lock();
    try {
        // 业务逻辑
    } finally {
        lock.unlock(); // 确保执行
    }

  2. 锁的重入次数限制‌:
    ReentrantLock 的最大重入次数为 Integer.MAX_VALUE。虽然极少触及,但在极端递归场景下需注意 Error: Maximum lock count exceeded

  3. 公平锁的性能代价‌:
    公平锁为了保证 FIFO 顺序,每次加锁都需要检查队列,这会导致大量的上下文切换和线程挂起/唤醒操作。在高吞吐场景下,‌非公平锁的性能通常远高于公平锁‌。除非有严格的顺序需求,否则建议使用默认的非公平锁。

  4. 避免锁泄露‌:
    确保 lock() 和 unlock() 成对出现。不要在 try 块之前加锁,也不要在 catch 块中遗漏解锁。

  5. Condition 的正确使用‌:
    使用 condition.await() 时,必须确保当前线程持有锁,否则会抛出 IllegalMonitorStateException。同样,await() 会释放锁并挂起线程,被唤醒后会重新竞争锁。

总结

ReentrantLock 是 Java 并发编程中强大且灵活的工具。它基于 AQS 框架,通过 CAS 和 CLH 队列实现了高效的并发控制。虽然使用复杂度高于 synchronized,但其提供的‌公平性选择、可中断、超时控制和多条件变量‌等功能,使其成为解决复杂并发问题(如生产者-消费者模型、读写分离、分布式锁本地实现等)的首选方案。掌握其底层原理和使用规范,是进阶 Java 高级开发的必经之路。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值