文章目录

Java 锁重入与条件变量在ReentrantLock中的实现

发布于 2026-04-03 00:48:33 · 浏览 8 次 · 评论 0 条

Java 锁重入与条件变量在 ReentrantLock 中的实现

Java 提供了 ReentrantLock 类作为内置锁(synchronized)的替代方案,它支持更灵活的锁控制。其中两个核心特性是锁重入条件变量。理解它们的实现机制,能帮助你写出更高效、安全的并发代码。


什么是锁重入?

锁重入是指同一个线程可以多次获取同一把锁而不会发生死锁。例如,一个方法 A 调用了另一个也使用相同锁的方法 B,只要这两个方法由同一线程执行,就能顺利运行。

验证锁重入能力

  1. 创建一个 ReentrantLock 实例。
  2. 调用 lock() 方法两次(在同一线程中)。
  3. 确保最终调用两次 unlock() 才能完全释放锁。
import java.util.concurrent.locks.ReentrantLock;

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

    public void outer() {
        lock.lock();
        try {
            System.out.println("Outer locked");
            inner(); // 同一线程再次获取锁
        } finally {
            lock.unlock();
        }
    }

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

上述代码能正常运行,说明 ReentrantLock 支持重入。


ReentrantLock 如何实现锁重入?

ReentrantLock 内部通过一个计数器state)和持有线程引用来实现重入:

  • 当线程首次获取锁时,state 设为 1,并记录当前线程。
  • 若同一线程再次调用 lock()state 自增。
  • 每次调用 unlock()state 自减。
  • 只有当 state 回到 0 时,锁才真正释放,其他线程才有机会获取。

这个机制依赖于 AbstractQueuedSynchronizer(AQS),它是 Java 并发包的核心同步框架。


条件变量是什么?为什么需要它?

条件变量允许线程在某个条件不满足时挂起等待,直到其他线程通知条件已满足。这比轮询(busy-waiting)更节省 CPU 资源。

ReentrantLock 中,通过 newCondition() 方法创建条件变量,返回一个 Condition 对象。

典型使用场景:生产者-消费者模型。


使用 Condition 实现线程协作

实现一个有界缓冲区,支持生产者写入、消费者读取,并在线程安全的前提下阻塞等待:

  1. 定义一个固定大小的数组作为缓冲区。
  2. 创建一个 ReentrantLock 和一个关联的 Condition(如 notFullnotEmpty)。
  3. 生产者在缓冲区满时调用 await() 等待。
  4. 消费者在缓冲区空时调用 await() 等待。
  5. 每次操作后,根据状态调用 signal()signalAll() 唤醒等待线程。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class BoundedBuffer {
    final Object[] items = new Object[10];
    int putIndex, takeIndex, count;
    final ReentrantLock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition();
    final Condition notEmpty = lock.newCondition();

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) {
                notFull.await(); // 缓冲区满,等待
            }
            items[putIndex] = x;
            if (++putIndex == items.length) putIndex = 0;
            ++count;
            notEmpty.signal(); // 通知消费者可以取了
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await(); // 缓冲区空,等待
            }
            Object x = items[takeIndex];
            if (++takeIndex == items.length) takeIndex = 0;
            --count;
            notFull.signal(); // 通知生产者可以放了
            return x;
        } finally {
            lock.unlock();
        }
    }
}

关键点:

  • 必须在持有锁的情况下调用 await()signal(),否则会抛出 IllegalMonitorStateException
  • 使用 while 而非 if 判断条件,防止虚假唤醒(spurious wakeup)。

Condition 的内部机制

每个 Condition 对象内部维护一个等待队列(不同于 AQS 的同步队列)。调用 await() 时:

  1. 释放当前持有的锁(自动调用 unlock 效果)。
  2. 将当前线程封装成节点,加入该 Condition 的等待队列。
  3. 阻塞线程,直到被 signal() 唤醒。
  4. 被唤醒后,重新竞争锁,成功获取后继续执行。

调用 signal() 时:

  1. 从等待队列中取出第一个节点
  2. 将其移入 AQS 的同步队列,参与锁的竞争。

这种设计实现了“等待-通知”机制与锁的解耦,允许多个条件变量共享同一把锁。


锁重入与 Condition 的交互

由于 ReentrantLock 支持重入,同一个线程可以在持有锁的情况下多次调用 lock(),但调用 await()完全释放锁(释放所有重入次数),并在被唤醒后重新获取全部重入次数

例如:

  • 线程 T 已重入 3 次(state = 3)。
  • T 调用 condition.await()state 变为 0,T 进入等待队列。
  • 其他线程可获取锁。
  • 当 T 被 signal() 唤醒并重新获得锁后,state 恢复为 3。

这一行为保证了语义一致性:await() 前后,线程对锁的“持有深度”不变。


常见错误与最佳实践

错误做法 正确做法
synchronized 块中使用 Condition 只在 ReentrantLock 保护下使用 Condition
if 判断条件后调用 await() 始终用 while 循环检查条件
忘记在 finally 块中 unlock() lock() 后必须配对 unlock(),且放在 finally
调用 signal() 后不释放锁(长时间持有) 尽快释放锁,避免阻塞其他线程

性能与适用场景

  • 高竞争场景ReentrantLock 提供公平锁选项(构造时传 true),可减少线程饥饿,但吞吐量略低。
  • 需要超时或中断ReentrantLock 支持 tryLock(timeout, unit) 和可中断的 lockInterruptibly(),比 synchronized 更灵活。
  • 多条件等待:若需多个等待条件(如“队列满”、“队列空”、“任务完成”),Condition 比单个 wait/notify 更清晰。

选择建议

  • 简单同步 → 用 synchronized
  • 需要高级功能(超时、多条件、公平性)→ 用 ReentrantLock + Condition

验证你的理解:动手测试

编写一个测试用例,验证重入和条件变量协同工作:

  1. 启动两个线程:一个生产者、一个消费者。
  2. 生产者连续放入 5 个元素。
  3. 消费者逐个取出并打印。
  4. 确保程序不卡死、无异常、输出顺序正确。
public class TestBoundedBuffer {
    public static void main(String[] args) throws InterruptedException {
        BoundedBuffer buffer = new BoundedBuffer();

        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    buffer.put("Item-" + i);
                    System.out.println("Produced: Item-" + i);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread consumer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    Object item = buffer.take();
                    System.out.println("Consumed: " + item);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.start();
        consumer.start();

        producer.join();
        consumer.join();
    }
}

运行结果应交替输出“Produced”和“Consumed”,证明锁重入和条件变量正常工作。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文