源本科技 | 码上会

Java 可重入锁

2026/01/29
23
0

学习目标

  • 理解 ReentrantLock 的核心特性与工作原理

  • 掌握其基本使用方法及关键 API

  • 能够正确实现可重入、公平性、中断响应等高级功能

  • 明确 ReentrantLock 相较于 synchronized 的优势与适用场景


什么是 ReentrantLock

ReentrantLock 是 Java 并发包(java.util.concurrent.locks)中提供的一个可重入互斥锁,它实现了 Lock 接口,为开发者提供了比 synchronized 关键字更灵活、更强大的线程同步机制。所谓“可重入”(Reentrant),是指同一个线程可以多次获取同一把锁而不会导致死锁。每次 lock() 调用都会增加持有计数,必须通过相同次数的 unlock() 才能真正释放锁。


基本使用方式

import java.util.concurrent.locks.ReentrantLock;

/**
 * 共享资源类,使用 ReentrantLock 保证线程安全
 */
public class SharedResource {
    private final ReentrantLock lock = new ReentrantLock();

    /**
     * 执行受保护的任务(临界区操作)
     */
    public void performTask() {
        lock.lock(); // 获取锁:若已被其他线程持有,则当前线程阻塞等待
        try {
            // 临界区:同一时间只允许一个线程执行此段代码
            System.out.println(Thread.currentThread().getName() + " 正在执行任务");
            
            // 模拟实际工作(可选)
            // Thread.sleep(500);
            
        } finally {
            lock.unlock(); // 必须在 finally 块中释放锁,防止异常导致死锁
        }
    }
}

重要原则unlock() 必须放在 finally 块中,确保即使发生异常也能释放锁,防止死锁。


可重入行为演示

以下示例展示了同一个线程如何安全地多次获取同一把锁:

import java.util.concurrent.locks.ReentrantLock;

/**
 * 演示 ReentrantLock 的可重入特性:
 * 同一个线程可以多次获取同一把锁,而不会发生死锁。
 */
public class ReentrantDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void methodA() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 进入方法 A");
            methodB(); // 同一线程再次请求同一把锁(可重入)
        } finally {
            lock.unlock();
        }
    }

    public void methodB() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 进入方法 B");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantDemo demo = new ReentrantDemo();
        // 在主线程中调用 methodA,触发可重入行为
        demo.methodA();
    }
}

输出:

main 进入方法 A
main 进入方法 B

说明:
主线程在 methodA() 中已持有锁,调用 methodB() 时再次请求同一把锁。由于 ReentrantLock 支持可重入,该操作成功,持有计数变为 2。只有当两次 unlock() 都执行后,锁才被完全释放。


公平性策略

ReentrantLock 支持两种调度策略:

  • 非公平锁(默认):允许插队,新来的线程可能比等待队列中的线程先获得锁,吞吐量更高

  • 公平锁:严格按照请求顺序分配锁(FIFO),避免线程饥饿,但性能略低。

// 创建公平锁
ReentrantLock fairLock = new ReentrantLock(true);

// 创建非公平锁(默认)
ReentrantLock unfairLock = new ReentrantLock(); // 等价于 new ReentrantLock(false);

建议:除非有明确的公平性需求(如任务调度系统),否则优先使用默认的非公平锁以获得更好性能。


核心方法

方法

描述

void lock()

获取锁;若已被占用,则阻塞等待

void unlock()

释放锁;持有计数减 1,归零时真正释放

boolean tryLock()

非阻塞尝试获取锁,立即返回 true(成功)或 false(失败)

boolean tryLock(long timeout, TimeUnit unit)

在指定时间内尝试获取锁,超时则返回 false

void lockInterruptibly()

获取锁,但允许在等待过程中被中断(抛出 InterruptedException

int getHoldCount()

返回当前线程对该锁的持有次数

boolean isHeldByCurrentThread()

判断当前线程是否持有该锁

boolean hasQueuedThreads()

检查是否有线程正在等待获取此锁

boolean isLocked()

判断是否有任意线程持有此锁

Condition newCondition()

创建与此锁关联的 Condition 对象,用于线程间协作

示例:使用 tryLock 避免长时间阻塞

if (lock.tryLock()) {
    try {
        // 执行临界区操作
    } finally {
        lock.unlock();
    }
} else {
    // 锁已被占用,执行备选逻辑(如记录日志、重试或跳过)
    System.out.println("Failed to acquire lock, skipping operation.");
}

ReentrantLock vs synchronized

特性

synchronized

ReentrantLock

可重入性

✅ 支持

✅ 支持

显式加锁 / 解锁

❌ 隐式

✅ 需手动调用 lock() / unlock()

可中断等待

❌ 不支持

✅ 支持(lockInterruptibly()

尝试获取锁

❌ 不支持

✅ 支持(tryLock()

公平性控制

❌ 无

✅ 可配置(构造函数参数)

多条件变量

❌ 仅一个隐式条件

✅ 支持多个 Condition

锁状态查询

❌ 无法查询

✅ 提供 isLocked(), getHoldCount() 等方法

使用复杂度

简单、不易出错

灵活但需谨慎管理锁生命周期


最佳实践

  1. 优先考虑 synchronized:对于简单同步需求,synchronized 更简洁、安全,且 JVM 对其有深度优化。

  2. 仅在需要高级功能时使用 ReentrantLock:如超时获取、可中断、公平性或多条件等待。

  3. 始终在 finally 块中释放锁:防止因异常导致锁未释放,引发死锁。

  4. 避免嵌套锁顺序不一致:若需获取多把锁,应固定获取顺序以防止死锁。

  5. 慎用公平锁:除非业务逻辑严格要求 FIFO,否则非公平锁性能更优。


重点总结

  • ReentrantLock 是一个功能强大、可重入的显式锁,提供比 synchronized 更精细的控制能力。

  • 其核心优势在于可中断、可超时、可尝试、可公平的锁获取机制。

  • 可重入特性允许同一线程多次持有锁,适用于递归调用或方法链场景。

  • 尽管功能丰富,但使用不当(如忘记 unlock())易引发死锁,需格外谨慎。

  • 在现代 Java 开发中,ReentrantLock 是构建高性能、高可靠并发系统的重要工具之一。


思考题

  1. 如果一个线程在持有 ReentrantLock 期间被中断,会发生什么?lockInterruptibly() 与普通 lock() 在中断处理上有何本质区别?

  2. 在什么场景下你会选择使用公平锁?请结合实际系统(如任务队列、资源分配器)举例说明。

  3. 设计一个银行转账系统,使用 ReentrantLock 实现账户间的线程安全转账,并说明如何避免死锁(提示:按账户 ID 排序获取锁)。