ReentrantLock自编

一、lockInterruptibly使用

 

lock.lockInterruptibly() 是 Java 并发包 (java.util.concurrent.locks) 中 Lock 接口提供的一个重要方法,它提供了一种可中断的锁获取机制。与基本的 lock() 方法相比,它在锁获取过程中增加了对线程中断(interrupt())的响应能力。

 

核心特性与工作原理

 

  1. 可中断的锁获取:

    • 当线程尝试通过 lock.lockInterruptibly() 获取锁时:

      • 如果锁可用,线程会立即获取锁并继续执行(返回)。

      • 如果锁不可用,线程会进入等待状态(类似于 synchronized 中的阻塞状态)。

    • 关键区别:在等待锁的过程中,如果其他线程调用了此线程的 interrupt() 方法,该等待线程会立即响应中断,停止等待锁,并抛出 InterruptedException

  2. 对比 lock.lock()

    • 普通的 lock.lock() 方法在获取锁时是不可中断的:

      • 如果锁不可用,线程会一直阻塞等待。

      • 即使线程被中断 (interrupt()),它依然会继续等待锁,直到成功获取锁。获取锁后,线程的中断状态会被设置(可以通过 Thread.interrupted() 检测到),但不会抛出异常。

      • 中断操作只在线程成功获取锁之后才能被处理(通过检查中断状态)。

 

为什么需要 lockInterruptibly()

 

  1. 增强响应性:

    • 在需要长时间等待锁的场景下(如死锁风险高、资源竞争激烈),lockInterruptibly() 允许外部通过中断机制主动取消等待锁的线程,避免线程无限期阻塞。

    • 这对于实现可取消的任务、超时控制(结合中断)或系统关闭时的资源清理至关重要。

  2. 构建更灵活的同步结构:

    • 它是实现 Condition 的 await() 方法(可中断的等待)以及其他高级同步工具(如 Semaphore.acquire())的基础。

 

方法签名

 

java
 
复制
 
下载
void lockInterruptibly() throws InterruptedException;

 

  • 返回值:void

  • 抛出异常:InterruptedException - 当等待锁的线程被中断时抛出。

 

使用模式

 

使用 lockInterruptibly() 的标准模式如下:

Lock lock = new ReentrantLock(); // 通常使用 ReentrantLock

try {
    // 尝试获取锁(可中断)
    lock.lockInterruptibly(); // 可能抛出 InterruptedException
    try {
        // 临界区代码:访问共享资源
        // ...
    } finally {
        // 无论是否发生异常,确保释放锁
        lock.unlock();
    }
} catch (InterruptedException e) {
    // 处理中断:线程在等待锁时被中断
    // 1. 清理状态(如有必要)
    // 2. 通常需要恢复中断状态(因为捕获异常会清除中断标志)
    Thread.currentThread().interrupt(); // 恢复中断状态
    // 3. 根据业务逻辑决定是退出、重试还是其他操作
    // 例如:抛出 CancellationException 或直接返回
}

 

关键注意事项

 

  1. 必须在 finally 中释放锁:

    • 与 lock.lock() 一样,获取锁后必须通过 lock.unlock() 释放,且应放在 finally 块中确保锁一定被释放,防止死锁。

  2. 正确处理 InterruptedException

    • 不要忽略此异常!这是通知你线程被取消的信号。

    • 恢复中断状态:调用 Thread.currentThread().interrupt() 重新设置线程的中断标志。这是 Java 并发编程的最佳实践,因为捕获 InterruptedException 会清除中断状态,上层代码可能需要知道中断发生了。

    • 执行清理:根据任务性质,可能需要回滚操作、释放其他资源或通知相关组件。

    • 决定后续行为:通常是退出当前任务(如抛出 CancellationException 或直接返回)。

  3. 不可中断的锁获取:

    • 如果你需要不可中断的锁获取行为,应使用 lock.lock()

  4. 与 Condition 配合:

    • Lock 创建的 Condition 对象(lock.newCondition())的 await() 方法本身也是可中断的,其行为与 lockInterruptibly() 在中断响应上类似。

 

示例:可取消的任务

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

    public void performTask() {
        try {
            lock.lockInterruptibly(); // 可中断地获取锁
            try {
                System.out.println(Thread.currentThread().getName() + " acquired the lock");
                // 模拟长时间工作(期间可被中断)
                while (!Thread.currentThread().isInterrupted()) {
                    // 工作... 这里用 sleep 模拟
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " working...");
                }
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + " released the lock");
            }
        } catch (InterruptedException e) {
            // 等待锁或工作期间被中断
            System.out.println(Thread.currentThread().getName() + " was INTERRUPTED while waiting or working");
            Thread.currentThread().interrupt(); // 恢复中断状态
        }
    }

    public static void main(String[] args) throws InterruptedException {
        InterruptibleLockExample example = new InterruptibleLockExample();

        Thread worker = new Thread(() -> example.performTask(), "Worker-Thread");
        worker.start();

        // 主线程等待一会,然后中断工作线程
        Thread.sleep(3000);
        System.out.println("Main thread interrupting worker...");
        worker.interrupt(); // 中断工作线程(无论它在等锁还是工作中)
        worker.join();
    }
}

 

总结

 

lock.lockInterruptibly() 是 Lock 接口提供的核心方法之一,它实现了可中断的锁获取:

 

  1. 核心价值:允许线程在等待锁的过程中响应中断请求(interrupt()),提高程序的响应性和可控性。

  2. 与 lock() 的区别:lock() 不可中断,即使被中断也会继续等待锁;lockInterruptibly() 在等待时被中断会抛出 InterruptedException

  3. 使用要点:

    • 必须用 try-catch 捕获 InterruptedException

    • 必须在 finally 块中释放锁 (unlock())。

    • 捕获中断异常后,应恢复中断状态 (Thread.currentThread().interrupt()) 并执行必要的清理。

  4. 适用场景:需要支持任务取消、超时控制或系统优雅关闭的并发程序。

 

它是构建健壮、响应灵敏的并发系统的重要工具,尤其在替代传统的 synchronized + wait()/notify() 机制时提供了更强大的中断控制能力。

 

二、Condition使用

1. ReentrantLock(可重入锁)

含义:
Java 并发包中的互斥锁实现,提供比 synchronized 更精细的控制:

  • 可重入性:同一线程可多次获取同一把锁

  • 公平性:支持公平锁(先等待的线程先获取)和非公平锁(默认)

  • 锁操作:

    • lock():阻塞获取锁

    • unlock():释放锁(必须在 finally 中调用)

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

    • lockInterruptibly():可中断获取锁

 
 
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区操作
} finally {
    lock.unlock(); // 确保释放锁
}

2. Condition(条件变量)

含义:
由 ReentrantLock 创建的线程等待/唤醒控制器:

  • 一个锁可创建多个 Condition(如:notFullnotEmpty

  • 每个 Condition 管理一个独立的等待队列

  • 解决 synchronized 单一等待队列的局限性

 
 
Condition condition = lock.newCondition(); // 从锁创建条件

3. await()(条件等待)

含义:
Condition 的方法,让线程释放锁并进入等待:

  • 调用前提:必须持有锁(否则抛 IllegalMonitorStateException

  • 行为:

    1. 释放锁

    2. 线程进入该条件的等待队列

    3. 被唤醒后重新竞争锁

  • 必须用 while 循环检查条件(防虚假唤醒)

 
 
lock.lock();
try {
    while (!conditionMet) {  // 循环检查条件
        condition.await();   // 释放锁并等待
    }
    // 条件满足后的操作
} finally {
    lock.unlock();
}

4. signal() 与 signalAll()(条件唤醒)

方法含义区别
signal() 唤醒同一条件队列中的一个线程 随机唤醒一个等待线程
signalAll() 唤醒同一条件队列中的所有线程 唤醒全部线程竞争锁

共同点:

  • 调用前提:必须持有锁

  • 行为:将等待线程移到锁的同步队列(唤醒后需重新获取锁)

 
lock.lock();
try {
    conditionMet = true;     // 改变共享状态
    condition.signalAll();   // 唤醒所有等待线程
} finally {
    lock.unlock();
}

关键区别对比

组件/方法作用与 synchronized 对比优势
ReentrantLock 互斥访问共享资源 支持公平锁、可中断、多条件
Condition 管理特定条件的等待队列 多队列精准控制(如:非满/非空)
await() 释放锁并等待条件 可绑定特定条件,支持超时等待
signal() 唤醒一个等待线程 精确唤醒目标队列的线程
signalAll() 唤醒所有等待线程 避免线程饿死(推荐使用)

为什么使用这种组合?

  1. 解耦等待条件

    • 传统 synchronized:所有线程在单一等待队列混等

    • Condition:生产者等 notFull,消费者等 notEmpty

  2. 精准唤醒

    • 生产者只需唤醒消费者(notEmpty.signalAll()

    • 避免无效唤醒(如 synchronized 的 notifyAll() 会唤醒生产者+消费者)

  3. 高性能

    • 减少线程竞争(不同条件独立等待)

    • 避免 "惊群效应"(signal() 精确唤醒一个)


最佳实践

  1. 锁释放规范

    lock.lock();
    try {
        // 操作共享资源
    } finally {
        lock.unlock(); // 绝对执行
    }
  2. 条件检查范式

     
    while (!condition) { // 必须用 while,不能用 if!
        condition.await();
    }
  3. 唤醒策略选择

    • 优先 signalAll():安全唤醒所有相关线程(推荐)

    • 谨慎 signal():确保不会导致线程饿死

  4. 超时控制

     
    if (condition.await(1, TimeUnit.SECONDS)) {
        // 正常唤醒
    } else {
        // 超时处理
    }

典型应用场景

    1. 阻塞队列(如 ArrayBlockingQueue

    2. 线程池任务调度

    3. 生产者-消费者系统

    4. 资源池管理(数据库连接池)

    5. 有限状态机控制

      通过 ReentrantLock + Condition + await()/signal()/signalAll() 的组合,可实现比 synchronized 更高效、更可控的线程协作,尤其适合构建高性能并发数据结构。

 

3、tryLock()使用

一、tryLock() 的核心概念

tryLock() 是 ReentrantLock 提供的非阻塞式锁获取方法,与常规的 lock() 方法有本质区别:

方法行为特性是否阻塞返回值
lock() 阻塞直到获取锁 void
tryLock() 尝试立即获取锁 boolean
tryLock(timeout) 限时尝试获取锁 可能阻塞 boolean

二、tryLock() 的方法签名

// 立即尝试获取锁(非阻塞)
boolean tryLock();

// 带超时的尝试获取锁(可能阻塞)
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;

四、典型使用场景

1. 避免死锁(锁顺序获取)

public void transfer(Account from, Account to, int amount) {
    while (true) {
        if (from.lock.tryLock()) {
            try {
                if (to.lock.tryLock()) {
                    try {
                        from.withdraw(amount);
                        to.deposit(amount);
                        return; // 转账成功
                    } finally {
                        to.lock.unlock();
                    }
                }
            } finally {
                from.lock.unlock();
            }
        }
        // 随机退避避免活锁
        Thread.sleep(random.nextInt(100)); 
    }
}

2. 快速失败机制

public void processTask() {
    if (!lock.tryLock()) {
        log.warn("资源忙,跳过本次处理");
        return;
    }
    
    try {
        // 执行关键操作
    } finally {
        lock.unlock();
    }
}

3. 带超时的操作

public void performTimedOperation() {
    try {
        if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
            try {
                // 在500ms内获取到锁
            } finally {
                lock.unlock();
            }
        } else {
            // 超时处理
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        // 中断处理
    }
}

五、tryLock() 的特性详解

  1. 非公平性优先:

    • 即使设置为公平锁,tryLock() 也会忽略公平性原则

    • 只要锁可用就立即获取,不考虑等待队列

  2. 可中断性:

    • 无参 tryLock() 不可中断

    • 带超时的 tryLock() 可响应中断

  3. 重入性支持:

    • 与 lock() 一样支持可重入

    • 同一线程可多次调用 tryLock()

  4. 与条件变量的关系:

    • tryLock() 不能与 Condition 直接配合使用

    • 创建 Condition 需要先持有锁

六、与 lock() 的对比决策

场景推荐方法原因
必须获取锁才执行操作 lock() 保证操作执行
避免死锁的锁排序 tryLock() 实现锁排序获取
高并发下的非关键操作 tryLock() 避免阻塞,保持系统响应性
需要精确控制等待时间 tryLock(timeout) 设置最大等待阈值
需要严格公平性的场景 lock() tryLock() 忽略公平性

七、最佳实践

  1. 必须检查返回值

    if (lock.tryLock()) { // 必须检查结果!
        try { /* 操作 */ } 
        finally { lock.unlock(); }
    } else {
        // 后备方案
    }
  2. 避免嵌套 tryLock 的活锁

     
    // 错误示例:可能造成活锁
    while (!lock1.tryLock() || !lock2.tryLock()) {
        // 空循环
    }
    
    // 正确做法:添加随机退避
    while (!lock1.tryLock()) {
        Thread.sleep(random.nextInt(50));
    }
  3. 结合中断处理

    try {
        if (lock.tryLock(1, TimeUnit.SECONDS)) {
            // ...
        }
    } catch (InterruptedException e) {
        // 恢复中断状态
        Thread.currentThread().interrupt(); 
        // 执行清理操作
    }
  4. 资源清理保证

    boolean acquired = false;
    try {
        acquired = lock.tryLock();
        if (acquired) {
            // 操作资源
        }
    } finally {
        if (acquired) lock.unlock(); // 仅当获取成功才释放
    }

八、性能考量

  1. 高竞争场景:

    • 频繁的 tryLock() 调用可能增加 CPU 开销

    • 在锁竞争激烈时,使用队列策略更高效

  2. 低竞争场景:

    • tryLock() 可减少线程切换开销

    • 尤其适合短时临界区操作

  3. 监控建议:

     
    long start = System.nanoTime();
    boolean acquired = lock.tryLock(100, TimeUnit.MILLISECONDS);
    long elapsed = System.nanoTime() - start;
    
    if (acquired) {
        metrics.recordLockWaitTime(elapsed);
    } else {
        metrics.recordLockTimeout();
    }

九、典型应用案例

  1. 高并发计数器

class OptimisticCounter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count;
    
    public void increment() {
        if (!lock.tryLock()) {
            // 快速路径失败时使用备用策略
            synchronized (this) {
                count++;
            }
            return;
        }
        
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}
  1. 资源池实现

 
class ResourcePool {
    private final ReentrantLock lock = new ReentrantLock();
    private final Queue<Resource> pool = new LinkedList<>();
    
    public Resource acquire(long timeoutMs) throws TimeoutException {
        try {
            if (lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) {
                try {
                    while (pool.isEmpty()) {
                        // 等待资源可用...
                    }
                    return pool.poll();
                } finally {
                    lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        throw new TimeoutException("获取资源超时");
    }
}

十、总结

ReentrantLock.tryLock() 提供了比传统锁获取更灵活的机制:

  1. 非阻塞特性:立即返回获取结果,避免线程阻塞

  2. 超时控制:设置最大等待时间,防止无限期等待

  3. 死锁预防:实现安全的锁排序获取策略

  4. 系统响应性:保持高负载下的系统响应能力

正确使用 tryLock() 需要:

  • 严格检查返回值

  • 合理处理失败场景

  • 结合超时和中断机制

  • 避免活锁问题

在需要精细控制锁获取行为的并发场景中,tryLock() 是实现高性能、高可靠性系统的关键工具之一。

posted @ 2025-05-31 18:03  飘来荡去evo  阅读(36)  评论(0)    收藏  举报