ReentrantLock 详解

170 阅读4分钟

ReentrantLock 详解


一、核心特性

ReentrantLock 是 Java 并发包(java.util.concurrent)中提供的显式可重入锁,相比 synchronized,其优势在于更灵活的锁控制和更丰富的功能:

  1. 可重入性:同一线程可多次获取锁。
  2. 公平性选择:支持公平锁和非公平锁(默认非公平锁)。
  3. 可中断锁:线程等待锁时可响应中断。
  4. 超时机制:支持尝试获取锁(tryLock)。
  5. 多条件变量:通过 Condition 实现多条件等待队列。

二、实现原理

ReentrantLock 基于 AQS(AbstractQueuedSynchronizer) 实现,核心依赖以下机制:

1. AQS 的核心组件

状态变量(state : • 表示锁的持有次数。 • 当 state = 0 时,锁未被占用;state > 0 时,锁被占用,数值表示重入次数。 • CLH 队列: • 一个虚拟的双向队列,存储等待锁的线程。 • 通过 Node 节点封装线程和状态(如等待状态)。

2. 锁的获取与释放

非公平锁(默认)

final void lock() {
    if (compareAndSetState(0, 1)) // 尝试直接获取锁
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1); // 进入队列等待
}

流程: 1. 直接尝试通过 CAS 获取锁。 2. 若失败,调用 acquire() 进入队列等待。

公平锁

final void lock() {
    acquire(1); // 直接进入队列等待
}

流程: 1. 检查队列中是否有等待线程。 2. 若无,尝试 CAS 获取锁;若有,排队等待。

3. 可重入性实现

获取锁: 当前线程与持有锁的线程相同时,state 递增。

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires; // 重入时 state 递增
        setState(nextc);
        return true;
    }
    return false;
}

释放锁: 每次释放锁时 state 递减,直至 state = 0 时完全释放。

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

三、核心功能与使用场景

1. 公平锁与非公平锁

非公平锁(默认) : • 特点:允许线程插队,吞吐量高。 • 适用场景:高并发且对响应时间不敏感的场景(如缓存系统)。

公平锁: • 特点:按队列顺序获取锁,避免饥饿。 • 适用场景:对公平性要求高的场景(如交易系统)。

示例

// 创建公平锁
ReentrantLock fairLock = new ReentrantLock(true);
// 创建非公平锁(默认)
ReentrantLock nonfairLock = new ReentrantLock();
2. 可中断锁

方法lockInterruptibly()场景:线程等待锁时需响应中断(如用户取消操作)。 示例

ReentrantLock lock = new ReentrantLock();
try {
    lock.lockInterruptibly(); // 可中断的锁获取
    // 执行临界区代码
} catch (InterruptedException e) {
    // 处理中断
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}
3. 超时机制

方法tryLock(long timeout, TimeUnit unit)场景:避免死锁或长时间等待。 示例

if (lock.tryLock(3, TimeUnit.SECONDS)) {
    try {
        // 执行临界区代码
    } finally {
        lock.unlock();
    }
} else {
    // 处理超时
}
4. 多条件变量(Condition)

方法newCondition()场景:多条件等待(如生产者-消费者模型)。 示例

ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition(); // 队列未满条件
Condition notEmpty = lock.newCondition(); // 队列非空条件// 生产者
public void produce() {
    lock.lock();
    try {
        while (queue.isFull()) {
            notFull.await(); // 等待队列未满
        }
        queue.add(item);
        notEmpty.signal(); // 通知消费者
    } finally {
        lock.unlock();
    }
}

四、性能与优化

  1. 减少锁竞争: • 缩小锁的粒度(如分段锁)。 • 使用非公平锁提高吞吐量。
  2. 避免锁嵌套: • 在临界区内避免调用可能阻塞的方法(如 I/O 操作)。
  3. 优先使用无锁结构: • 结合 Atomic 类或 ConcurrentHashMap 减少锁使用。

五、对比 synchronized

特性ReentrantLocksynchronized
锁类型显式锁(需手动释放)隐式锁(自动释放)
公平性支持公平锁和非公平锁仅非公平锁
可中断性支持(lockInterruptibly()不支持
超时机制支持(tryLock不支持
多条件变量支持(Condition不支持(仅一个等待队列)
性能高竞争下更优低竞争下更优

六、适用场景总结

  1. 需要公平锁:如订单处理系统,确保先到先服务。
  2. 需超时或中断控制:如避免死锁或用户取消操作。
  3. 多条件协作:如生产者-消费者模型中的多条件等待。
  4. 复杂锁逻辑:如尝试获取锁、锁分段等高级需求。

七、代码示例

public class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

八、注意事项

  1. 必须手动释放锁: 在 finally 块中调用 unlock(),避免锁泄漏。
  2. 避免重复解锁: 确保当前线程持有锁后再调用 unlock()
  3. 锁的公平性选择: 公平锁可能降低吞吐量,仅在必要时使用。

通过合理使用 ReentrantLock,可以显著提升多线程程序的灵活性和性能,尤其适用于复杂的高并发场景。