Java 并发编程:ReentrantLock原理与实战详解

一、引言

在多线程编程中,线程安全始终是一个关键议题。Java 在早期版本中提供了synchronized关键字作为内置锁机制,以支持基本的同步控制。然而,随着并发程序复杂度的提高,synchronized的局限性日益显现,主要体现在以下几个方面:

  1. 功能受限synchronized 不支持尝试加锁、超时获取、可中断获取等高级功能。

  2. 缺乏灵活性:一旦进入临界区就只能等待,无法主动退出。

  3. 可观测性差:开发者无法获知线程是否处于等待队列,也无法干预唤醒策略。

为了解决这些问题,JDK 1.5 引入了 java.util.concurrent 包,引入了基于 AQS(AbstractQueuedSynchronizer) 构建的显式锁机制。其中,ReentrantLock 是最具代表性的实现之一,几乎可看作是 synchronized 的功能超集。

ReentrantLock 提供了以下关键特性,使其在高并发系统中广泛应用:

  • 可重入性:同一线程可多次获得同一把锁而不会被阻塞。

  • 公平与非公平策略:可控制线程获取锁的排队顺序。

  • 可中断获取:支持响应中断机制,提高线程管理灵活性。

  • 支持超时尝试获取锁:避免长时间阻塞。

  • 条件变量机制(Condition):支持多个等待队列,实现更细粒度的线程协作。

在 Java 8 中,ReentrantLock 的实现更加优化,对性能、吞吐量和可扩展性有显著提升。例如其内部采用了非阻塞算法(CAS)、锁竞争策略优化、线程节点复用等手段来提升效率。

synchronized相比,ReentrantLock虽然语法上更繁琐,但功能更强大、更灵活,尤其适用于如下场景:

  • 需要尝试获取锁或定时获取锁

  • 需要可中断的加锁过程

  • 需要实现多个条件队列

  • 对性能或锁的公平性有严格要求 

 二、ReentrantLock的核心特性

2.1 可重入性

可重入性(Reentrancy)是指同一线程在持有某把锁的情况下,如果再次请求该锁,不会被阻塞。

这是ReentrantLock名字的由来——它是“可重入锁”。其内部通过一个持有线程引用和一个重入计数器来实现:

public class ReentrantExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void outer() {
        lock.lock();
        try {
            System.out.println("outer acquired lock");
            inner();
        } finally {
            lock.unlock();
        }
    }

    public void inner() {
        lock.lock();
        try {
            System.out.println("inner also acquired lock");
        } finally {
            lock.unlock();
        }
    }
}

输出结果:

outer acquired lock
inner also acquired lock

说明锁是可重入的:同一线程嵌套调用两次lock()不会死锁。


2.2 公平锁与非公平锁

ReentrantLock通过构造函数指定公平性策略:

ReentrantLock fairLock = new ReentrantLock(true);  // 公平锁
ReentrantLock unfairLock = new ReentrantLock();    // 非公平锁(默认)
  • 公平锁:按照等待线程的先后顺序(FIFO)分配锁,避免插队。

  • 非公平锁:允许当前线程插队尝试获取锁,即使其他线程已在排队。

公平锁减少“饥饿”现象,更适合对响应时间有要求的系统;非公平锁可提升吞吐量,适用于高性能场景。

注意:公平性牺牲了一定的性能,因为涉及更多的排队管理逻辑。


2.3 可中断锁

可中断锁允许线程在等待获取锁期间响应中断信号。

使用方式:

lock.lockInterruptibly();

示例:

ReentrantLock lock = new ReentrantLock();

Thread t = new Thread(() -> {
    try {
        lock.lockInterruptibly();
        try {
            System.out.println("Thread acquired lock");
        } finally {
            lock.unlock();
        }
    } catch (InterruptedException e) {
        System.out.println("Thread interrupted while waiting for lock");
    }
});

t.start();
t.interrupt();  // 可中断

适用于在任务取消时需要退出锁等待队列的场景。


2.4 超时尝试锁

当需要尝试获取锁但不希望长时间阻塞时,可使用带超时机制的tryLock()方法:

if (lock.tryLock(2, TimeUnit.SECONDS)) {
    try {
        // 执行业务逻辑
    } finally {
        lock.unlock();
    }
} else {
    System.out.println("未能获取到锁,退出逻辑");
}

该方法可有效防止死锁或长时间阻塞,提高程序健壮性。

此外还有不带参数的重载:

if (lock.tryLock()) {
    // 非阻塞获取
}

2.5 条件变量(Condition)

ReentrantLock配合Condition实现线程之间的协调等待,功能上类似于synchronized中的wait()notify(),但支持多个等待队列。

创建方式:

Condition condition = lock.newCondition();

典型用法:

class TaskQueue {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();
    private final Queue<String> queue = new LinkedList<>();

    public void put(String task) {
        lock.lock();
        try {
            queue.offer(task);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public String take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await();
            }
            return queue.poll();
        } finally {
            lock.unlock();
        }
    }
}

通过多个Condition实例可构建复杂的线程协调机制,如读写队列、阶段控制等,是synchronized无法实现的。


以上五大特性构成了ReentrantLock区别于传统内置锁的基础。掌握它们的使用与原理,有助于我们更精细地控制并发逻辑、优化多线程性能。 

三、源码深度解析

在 Java 8 中,ReentrantLock 作为 java.util.concurrent.locks 包中的核心组件,底层依赖 AbstractQueuedSynchronizer(AQS) 构建了一个功能强大且灵活的锁实现框架。

3.1 继承结构

ReentrantLock 的核心继承与组合结构如下:

java.util.concurrent.locks.Lock(接口)
          ↑
  java.util.concurrent.locks.ReentrantLock(实现类)
                                      |
                            Sync(抽象静态内部类,继承AQS)
                              /           \
                     NonfairSync       FairSync

其中:

  • Lock:定义了基本的锁操作接口,如lock()unlock()tryLock()等。

  • ReentrantLock:真正的锁实现。

  • Sync:内部静态抽象类,继承AbstractQueuedSynchronizer

  • FairSync / NonfairSync:分别实现公平锁与非公平锁逻辑。

核心依赖的是 AQS 的模板方法机制,封装了线程排队、阻塞、唤醒等通用逻辑,子类只需实现锁状态的获取与释放。


3.2 lock()与unlock()方法分析

以非公平锁为例,其lock()方法逻辑如下:

public void lock() {
    sync.lock();
}

对应 NonfairSync 中实现:

final void lock() {
    if (compareAndSetState(0, 1)) {
        setExclusiveOwnerThread(Thread.currentThread());
    } else {
        acquire(1);
    }
}

解释:

  • compareAndSetState(0, 1):尝试抢占锁(CAS)。

  • 成功则设置当前线程为独占线程。

  • 否则进入AQS.acquire(1)排队。

释放锁:

public void unlock() {
    sync.release(1);
}

调用 AQS 的模板方法:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

核心方法 tryRelease()Sync 中实现,负责清空线程持有者与递减重入次数。


3.3 tryLock()与lockInterruptibly()

tryLock()
public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}

内部尝试通过 CAS 修改 state 为 1,同时设置独占线程,无需进入阻塞队列。

lockInterruptibly()
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

lock()不同的是,当线程被中断时会抛出InterruptedException,不会继续排队等待。


3.4 AQS核心机制解析

AQS 是构建 ReentrantLock 的底层框架,其核心成员包括:

  • volatile int state:同步状态。

  • Node headNode tail:双向队列头尾,构建阻塞线程链表。

  • Thread exclusiveOwnerThread:当前独占线程。

关键流程:

  • 加锁失败 → 构造 Node → 插入同步队列尾部 → 阻塞线程(LockSupport.park)

  • 释放锁state 归零 → 唤醒队列中的下一个节点

队列中的每个 Node 代表一个等待获取锁的线程,状态包括 SIGNALCANCELLED 等。


3.5 公平锁与非公平锁差异

非公平锁(默认)
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        setState(c + acquires);
        return true;
    }
    return false;
}
  • 当前线程若发现 state=0 立即抢占。

  • 支持可重入逻辑:当前线程再次加锁直接累加计数。

公平锁
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        setState(c + acquires);
        return true;
    }
    return false;
}

公平锁会先检查等待队列中是否有前置节点(即排队线程),有则不抢锁,确保 FIFO 顺序。 

四、实际使用示例

通过实战代码演示,进一步巩固对 ReentrantLock 各核心功能的理解。

4.1 基础用法:lock() / unlock()

public class BasicLockDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void doSomething() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在执行任务");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        BasicLockDemo demo = new BasicLockDemo();
        Runnable task = demo::doSomething;

        new Thread(task, "线程A").start();
        new Thread(task, "线程B").start();
    }
}

运行结果:两个线程互斥执行任务,保证了线程安全。


4.2 可重入性演示

public class ReentrantDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void outer() {
        lock.lock();
        try {
            System.out.println("outer 获得锁");
            inner();
        } finally {
            lock.unlock();
        }
    }

    public void inner() {
        lock.lock();
        try {
            System.out.println("inner 也获得锁");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        new ReentrantDemo().outer();
    }
}

控制台输出表明,线程可重复进入同一锁定代码块而不会死锁。


4.3 公平锁与非公平锁对比测试

public class FairLockTest {
    private static final ReentrantLock fairLock = new ReentrantLock(true);

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 2; i++) {
                fairLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " 获得锁");
                } finally {
                    fairLock.unlock();
                }
            }
        };

        for (int i = 1; i <= 5; i++) {
            new Thread(task, "线程" + i).start();
        }
    }
}

运行输出通常遵循创建线程的顺序(FIFO),证明公平策略生效。

new ReentrantLock(true) 改为 false 再测试,可观察到插队现象。


4.4 tryLock() 超时机制

public class TryLockTimeoutDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void work(String name) {
        try {
            if (lock.tryLock(2, TimeUnit.SECONDS)) {
                try {
                    System.out.println(name + " 成功获取锁");
                    Thread.sleep(3000);
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println(name + " 获取锁失败,放弃操作");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) {
        TryLockTimeoutDemo demo = new TryLockTimeoutDemo();
        new Thread(() -> demo.work("线程A")).start();
        new Thread(() -> demo.work("线程B")).start();
    }
}

控制台可能输出:

线程A 成功获取锁
线程B 获取锁失败,放弃操作

这体现了 tryLock(timeout) 的非阻塞特性。


4.5 Condition 的多线程协调

public class ConditionDemo {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private boolean ready = false;

    public void waitTask() {
        lock.lock();
        try {
            while (!ready) {
                System.out.println("等待任务准备...");
                condition.await();
            }
            System.out.println("任务已准备,继续执行");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }

    public void signalTask() {
        lock.lock();
        try {
            ready = true;
            condition.signal();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ConditionDemo demo = new ConditionDemo();
        Thread t1 = new Thread(demo::waitTask);
        t1.start();

        Thread.sleep(1000);
        demo.signalTask();
    }
}

运行效果:子线程等待,主线程发出信号唤醒。Condition 支持精准唤醒,是复杂线程通信场景的利器。 

五、最佳实践与陷阱规避

在使用 ReentrantLock 进行并发控制时,若未注意细节,可能会引发死锁、资源竞争或性能瓶颈等问题。本章总结实战中的常见陷阱与最佳实践。

5.1 避免死锁的策略

死锁通常由多个线程持有锁资源并相互等待造成。以下是常见的防止死锁的策略:

  • 固定加锁顺序:确保所有线程按照相同顺序获取多个锁。

  • 使用tryLock()尝试机制:避免线程长时间阻塞:

if (lock1.tryLock(1, TimeUnit.SECONDS)) {
    try {
        if (lock2.tryLock(1, TimeUnit.SECONDS)) {
            try {
                // 操作资源
            } finally {
                lock2.unlock();
            }
        }
    } finally {
        lock1.unlock();
    }
}
  • 资源回收机制:及时释放未成功获取的锁,避免长时间持有部分资源。

5.2 公平锁的性能权衡

公平锁虽然避免了“插队”,但在高并发环境下,频繁的排队与线程上下文切换会导致吞吐量下降:

  • 公平锁适用于响应时间敏感场景,如支付系统、交易平台等;

  • 非公平锁适合高吞吐优先的服务,如缓存、队列处理等。

建议:优先使用非公平锁,除非业务确有严格公平性需求。

5.3 避免遗漏 unlock()

lock()unlock() 必须成对出现,否则易造成死锁或锁泄露。推荐始终使用try-finally结构:

lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();
}

避免如下错误写法:

if (lock.tryLock()) {
    // 未释放锁,出错!
}

5.4 ReentrantLock vs synchronized 的选择

特性synchronizedReentrantLock
可中断锁
超时获取锁
公平锁支持
条件变量支持否(单一)是(多个Condition)
性能(高并发下)一般
使用便捷性简单略繁琐

建议:

  • 简单场景用 synchronized,如同步方法、代码块;

  • 高并发、精细化控制需求用 ReentrantLock

5.5 避免锁粒度过大

锁定范围过大会降低并发度,锁定范围过小则增加死锁概率。

建议:

  • 只需要同步的代码移入临界区;

  • 拆分对象粒度(如分段锁、分区锁)提高并发。

示例:使用多个锁保护不同资源,提高吞吐:

private final ReentrantLock lockA = new ReentrantLock();
private final ReentrantLock lockB = new ReentrantLock();

public void updateA() {
    lockA.lock();
    try {
        // 仅修改A
    } finally {
        lockA.unlock();
    }
}

public void updateB() {
    lockB.lock();
    try {
        // 仅修改B
    } finally {
        lockB.unlock();
    }
}

通过上述最佳实践的遵守与陷阱的规避,可以显著提升 ReentrantLock 在项目中的稳定性与性能,避免常见并发问题。 

六、总结

在并发编程日益成为主流开发场景的今天,ReentrantLock 作为 Java 8 并发工具包中最常用的显式锁之一,提供了比传统 synchronized 更强大和灵活的功能。

回顾全文,我们从其核心特性出发,深入解析了源码实现机制,再到具体的使用案例与实战演练,最后提出了实用的最佳实践建议,旨在帮助开发者全面掌握这一并发利器。

6.1 ReentrantLock 的优势归纳:

  • 支持可重入中断锁超时获取锁公平锁策略等高级特性;

  • 借助 Condition 实现多个条件队列,提升线程通信的表达能力;

  • 基于 AQS 构建,具备良好的可扩展性和性能表现;

  • 在高并发环境中相比 synchronized 更具性能优势。

6.2 推荐使用场景:

  • 对性能和并发控制有精细需求的系统(如缓存、线程池、消息队列);

  • 需要响应中断或定时控制的阻塞操作(如网络通信、数据库访问);

  • 多线程协作复杂、等待条件不止一类的业务逻辑(适合 Condition)。

6.3 展望与扩展

尽管 ReentrantLock 功能强大,但在 JDK 8 之后,也引入了新的锁机制如 StampedLockReadWriteLock

  • StampedLock 提供乐观读、悲观写、写升级机制,适合读多写少的场景;

  • ReadWriteLock 分离读写锁,提高并发读性能。

后续可针对不同业务特征,灵活选用锁机制,以达成性能与安全的平衡。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

探索java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值