跳转到正文
Siolin'Log
返回

Java Lock:ReentrantLock、可中断获取与 Condition

概述

Java 并发锁接口与常见实现关系

Java 中的 Lock 是一个接口,用于控制多个线程对共享资源的访问。与块结构化的 synchronized 相比,它允许在不同作用域中获取和释放锁,并提供非阻塞、可中断和带超时的获取方式。

Lock 只定义协议,不保证所有实现都可重入、独占或基于 AQS。本文以最常用的 ReentrantLock 为主要实现说明;读写锁等实现可能允许多个线程并发持有读锁。所有 Lock 实现的成功获取和释放操作,都必须提供与内置监视器锁相同的内存同步语义。

Lock 接口的核心方法

接口定义可以简化为:

public interface Lock {
    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

    boolean tryLock(long time, TimeUnit unit)
        throws InterruptedException;

    void unlock();

    Condition newCondition();
}

lock():阻塞式获取锁

基本用法:

private final Lock lock = new ReentrantLock();

public void update() {
    lock.lock();
    try {
        // 访问或修改共享数据
    } finally {
        lock.unlock();
    }
}

ReentrantLock 是可重入锁:当前线程已经持有锁时,可以再次获取它。每成功获取一次,都必须对应释放一次。

lock.lock(); // 第一次,持有次数为 1
lock.lock(); // 第二次,持有次数为 2

try {
    // 临界区
} finally {
    lock.unlock(); // 持有次数变为 1
    lock.unlock(); // 持有次数变为 0,真正释放
}

在当前 JDK 的 ReentrantLock 实现中,获取失败的线程会通过 AQS 进入同步等待队列;这属于 ReentrantLock 的实现细节,不是 Lock 接口对所有实现的要求。

ReentrantLock.lock() 不声明 InterruptedException,因此线程不能通过中断取消这次锁等待。如果等待期间收到中断,它仍会继续尝试获取锁;成功返回后,中断状态会被保留。

public void doSomething() {
    lock.lock(); // 排队时被中断,仍会继续等待,直到成功获取锁

    try {
        // 如果等待锁时收到过中断,此时中断状态仍为 true。
        // sleep() 会立即检测该状态并抛出 InterruptedException。
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        System.out.println("收到中断信号,开始处理善后工作...");
    } finally {
        lock.unlock();
    }
}

lockInterruptibly():可中断地获取锁

方法定义:

void lockInterruptibly() throws InterruptedException;

基本用法:

public void update() throws InterruptedException {
    lock.lockInterruptibly();
    try {
        doSomething();
    } finally {
        lock.unlock();
    }
}

它与 lock() 的区别是:线程等待锁时

lock()
    被中断 → 继续等待锁

lockInterruptibly()
    被中断 → 放弃等待并抛出 InterruptedException

对于 ReentrantLock,如果线程调用前中断状态已经为 true,会立即抛出 InterruptedException,而不是继续获取锁。

Thread.currentThread().interrupt();
lock.lockInterruptibly();

Thread.sleep()ReentrantLock.lockInterruptibly() 抛出 InterruptedException 时,都会清除当前线程的中断状态。上层如果不能完成中断处理,通常需要重新设置中断状态或继续抛出异常。

tryLock():非阻塞尝试获取锁

方法定义:

boolean tryLock();

它会立即尝试获取锁,不会进入长时间等待。

if (lock.tryLock()) {
    try {
        doSomething();
    } finally {
        lock.unlock();
    }
} else {
    System.out.println("获取锁失败");
}

即使 ReentrantLock 使用公平策略,无参 tryLock() 仍会在锁恰好可用时直接获取,可能绕过已经排队的线程。如果必须遵守公平策略,可以使用可中断的 tryLock(0, TimeUnit.SECONDS)

tryLock(time, unit):超时获取锁

方法定义:

boolean tryLock(long time, TimeUnit unit)
        throws InterruptedException;

它介于 lock()tryLock() 之间:

lock()                    一直等待
tryLock()                 完全不等待
tryLock(time, unit)       最多等待指定时间

带超时的 tryLock 同样可以响应中断。对于公平 ReentrantLock,它会遵守公平策略,不会在已有线程排队时直接插队。

unlock():释放锁

方法定义:

void unlock();

unlock() 不只是把一个布尔值改成“未锁定”。对于 ReentrantLock,它大致执行:

检查当前线程是否为锁持有者

重入计数减 1

计数是否为 0?
    ├─ 否:仍然持有锁
    └─ 是:清除持有者并唤醒等待线程

如果当前线程并未持有该 ReentrantLock,调用 unlock() 会抛出 IllegalMonitorStateException

这里也体现了锁和二值信号量的一个区别:

newCondition():创建条件队列

方法定义:

Condition newCondition();

它不会创建另一把锁,而是基于当前锁创建一个条件等待队列:

private final Lock lock = new ReentrantLock();

private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();

一把锁可以创建多个 Condition

同一把锁
  ├─ notEmpty 条件队列
  └─ notFull 条件队列

这比 synchronized 的单一等待集合更灵活。

调用 Condition.await()signal()signalAll() 时,当前线程必须持有关联的锁。await() 会原子地释放锁并进入条件队列;线程被唤醒后,还必须重新获取关联锁,成功后才能从 await() 返回。

条件等待允许出现虚假唤醒,因此判断条件时应使用 while,而不是只检查一次的 if

lock.lock();
try {
    while (queue.isEmpty()) {
        notEmpty.await();
    }
    consume(queue.remove());
} finally {
    lock.unlock();
}


下一篇
Java 并发问题:原子性、可见性与有序性