当前位置:  首页>> 技术小册>> Java并发编程实战

14 | Lock和Condition(上):隐藏在并发包中的管程

在Java并发编程的广阔天地中,LockCondition是构建高性能、可靠并发应用的核心构件。它们不仅提供了比传统synchronized关键字和wait()/notify()/notifyAll()方法更为灵活和强大的锁机制,还隐含着管程(Monitor)的设计思想,为复杂并发控制提供了坚实的基础。本章将深入探讨Lock接口及其相关的Condition接口,揭示它们如何作为Java并发包中隐藏的管程实现,以及如何利用这些工具来解决实际并发编程中的挑战。

1. 管程(Monitor)概述

在并发编程理论中,管程是一种用于管理共享资源的同步机制,它封装了共享数据以及对这些数据的操作,通过内部锁来保证同一时刻只有一个线程能够访问管程中的任意部分。管程的基本思想是将共享资源的访问权限封装在内部,任何线程想要访问这些资源,都必须通过管程提供的特定入口点进入,并在离开时释放资源。这种设计有效避免了并发冲突和数据不一致的问题。

Java的synchronized关键字和相关的wait()/notify()/notifyAll()方法,可以视为Java语言对管程概念的一种简化实现。然而,随着并发编程需求的复杂化,Java并发包(java.util.concurrent)提供了更为强大和灵活的LockCondition机制,以支持更细粒度的锁控制和更复杂的同步逻辑。

2. Lock接口

Lock接口是Java并发包中提供的一个核心接口,它代表了一个基本的锁机制。与synchronized不同,Lock需要显式地获取和释放锁,这提供了更大的灵活性,同时也要求程序员必须更加谨慎地管理锁的生命周期,以避免死锁等问题。

2.1 主要方法
  • void lock(): 获取锁。如果锁不可用,则当前线程将阻塞,直到锁可用。
  • void lockInterruptibly(): 可中断地获取锁。如果当前线程在等待锁的过程中被中断,则当前线程会抛出InterruptedException,并释放已占有的所有锁。
  • boolean tryLock(): 尝试获取锁,如果锁可用,则获取锁并返回true;如果锁不可用,则立即返回false,当前线程不会被阻塞。
  • boolean tryLock(long time, TimeUnit unit): 尝试在给定的时间内获取锁。如果锁在指定时间内变得可用,则返回true;如果锁在指定时间内始终不可用,则返回false。在等待过程中,当前线程可以被中断。
  • void unlock(): 释放锁。在调用此方法之前,当前线程必须持有此锁。
2.2 优势与用途
  • 灵活性Lock提供了比synchronized更多的灵活性,如尝试非阻塞地获取锁、可中断地获取锁等。
  • 条件变量Lock可以绑定多个Condition对象,每个Condition对象都管理着一个等待/通知队列,这使得能够更精细地控制线程的等待/唤醒行为。
  • 可重入性:大多数Lock实现都是可重入的,即同一个线程可以多次获得同一个锁。

3. Condition接口

Condition接口是Lock接口的一个补充,它提供了比Object监视器方法(wait()notify()notifyAll())更强大的线程间通信能力。每个Lock对象都可以关联多个Condition对象,这允许线程以不同的条件等待和唤醒,从而实现更为复杂的同步控制逻辑。

3.1 主要方法
  • void await(): 使当前线程等待直到另一个线程调用该条件的signal()signalAll()方法。在调用此方法之前,当前线程必须持有相关的锁。
  • void awaitUninterruptibly(): 与await()类似,但该方法对中断不敏感,即线程在等待过程中不会被中断。
  • long awaitNanos(long nanosTimeout): 使当前线程等待直到另一个线程调用该条件的signal()signalAll()方法,或者直到等待时间超过指定的纳秒数。
  • boolean awaitUntil(Date deadline): 使当前线程等待直到另一个线程调用该条件的signal()signalAll()方法,或者直到给定的截止时间到达。
  • void signal(): 唤醒等待在该条件上的一个线程(如果存在)。在调用此方法之前,当前线程必须持有相关的锁。
  • void signalAll(): 唤醒等待在该条件上的所有线程。在调用此方法之前,当前线程必须持有相关的锁。
3.2 相比Object监视器方法的优势
  • 多个等待集合:一个Lock可以有多个Condition实例,每个实例都管理着自己的等待队列,而Object监视器方法只有一个等待队列。
  • 更灵活的等待/通知控制:通过不同的Condition对象,可以基于不同的条件进行等待和唤醒,而Object监视器方法只能进行无差别的唤醒。
  • 更精细的锁定控制:由于ConditionLock紧密关联,因此可以在不释放锁的情况下进行等待,从而避免了在wait()notify()调用之间的重新获取锁的开销和潜在的竞争条件。

4. 应用实例:生产者-消费者问题

生产者-消费者问题是并发编程中的一个经典问题,用于演示线程间如何安全地共享和交换数据。利用LockCondition,我们可以更灵活地解决这个问题。

  1. import java.util.LinkedList;
  2. import java.util.Queue;
  3. import java.util.concurrent.locks.Condition;
  4. import java.util.concurrent.locks.Lock;
  5. import java.util.concurrent.locks.ReentrantLock;
  6. public class ProducerConsumerDemo {
  7. private final Queue<Integer> queue = new LinkedList<>();
  8. private final int capacity;
  9. private final Lock lock = new ReentrantLock();
  10. private final Condition notEmpty = lock.newCondition();
  11. private final Condition notFull = lock.newCondition();
  12. public ProducerConsumerDemo(int capacity) {
  13. this.capacity = capacity;
  14. }
  15. // 生产者方法
  16. public void produce(int value) throws InterruptedException {
  17. lock.lock();
  18. try {
  19. while (queue.size() == capacity) {
  20. notFull.await(); // 队列满时等待
  21. }
  22. queue.add(value);
  23. System.out.println("Produced: " + value);
  24. notEmpty.signal(); // 通知消费者
  25. } finally {
  26. lock.unlock();
  27. }
  28. }
  29. // 消费者方法
  30. public int consume() throws InterruptedException {
  31. lock.lock();
  32. try {
  33. while (queue.isEmpty()) {
  34. notEmpty.await(); // 队列空时等待
  35. }
  36. int value = queue.poll();
  37. System.out.println("Consumed: " + value);
  38. notFull.signal(); // 通知生产者
  39. return value;
  40. } finally {
  41. lock.unlock();
  42. }
  43. }
  44. // 主函数,用于演示
  45. public static void main(String[] args) {
  46. ProducerConsumerDemo demo = new ProducerConsumerDemo(10);
  47. // 启动生产者线程
  48. new Thread(() -> {
  49. for (int i = 0; i < 20; i++) {
  50. try {
  51. demo.produce(i);
  52. Thread.sleep(100); // 模拟生产耗时
  53. } catch (InterruptedException e) {
  54. Thread.currentThread().interrupt();
  55. }
  56. }
  57. }).start();
  58. // 启动消费者线程
  59. new Thread(() -> {
  60. for (int i = 0; i < 20; i++) {
  61. try {
  62. demo.consume();
  63. Thread.sleep(200); // 模拟消费耗时
  64. } catch (InterruptedException e) {
  65. Thread.currentThread().interrupt();
  66. }
  67. }
  68. }).start();
  69. }
  70. }

在上述示例中,我们使用了ReentrantLock作为锁实现,并通过它创建了两个Condition对象:notEmptynotFull,分别用于控制生产者在队列满时等待、消费者在队列空时等待,以及通知对方继续执行。这种实现方式比使用单个synchronized块和wait()/notify()方法更为清晰和灵活。

5. 总结

LockCondition作为Java并发包中隐藏的管程实现,为并发编程提供了强大而灵活的工具。通过显式地获取和释放锁,以及基于不同条件的等待/唤醒机制,它们能够支持更为复杂的同步控制逻辑。在实际应用中,合理利用LockCondition,可以显著提升并发程序的性能和可靠性。然而,也需要注意,过度的锁使用和不当的同步控制,可能会导致死锁、活锁等并发问题,因此在使用时务必谨慎。