在Java并发编程的广阔天地中,Lock
与Condition
是构建高性能、可靠并发应用的核心构件。它们不仅提供了比传统synchronized
关键字和wait()
/notify()
/notifyAll()
方法更为灵活和强大的锁机制,还隐含着管程(Monitor)的设计思想,为复杂并发控制提供了坚实的基础。本章将深入探讨Lock
接口及其相关的Condition
接口,揭示它们如何作为Java并发包中隐藏的管程实现,以及如何利用这些工具来解决实际并发编程中的挑战。
在并发编程理论中,管程是一种用于管理共享资源的同步机制,它封装了共享数据以及对这些数据的操作,通过内部锁来保证同一时刻只有一个线程能够访问管程中的任意部分。管程的基本思想是将共享资源的访问权限封装在内部,任何线程想要访问这些资源,都必须通过管程提供的特定入口点进入,并在离开时释放资源。这种设计有效避免了并发冲突和数据不一致的问题。
Java的synchronized
关键字和相关的wait()
/notify()
/notifyAll()
方法,可以视为Java语言对管程概念的一种简化实现。然而,随着并发编程需求的复杂化,Java并发包(java.util.concurrent
)提供了更为强大和灵活的Lock
和Condition
机制,以支持更细粒度的锁控制和更复杂的同步逻辑。
Lock
接口是Java并发包中提供的一个核心接口,它代表了一个基本的锁机制。与synchronized
不同,Lock
需要显式地获取和释放锁,这提供了更大的灵活性,同时也要求程序员必须更加谨慎地管理锁的生命周期,以避免死锁等问题。
void lock()
: 获取锁。如果锁不可用,则当前线程将阻塞,直到锁可用。void lockInterruptibly()
: 可中断地获取锁。如果当前线程在等待锁的过程中被中断,则当前线程会抛出InterruptedException
,并释放已占有的所有锁。boolean tryLock()
: 尝试获取锁,如果锁可用,则获取锁并返回true
;如果锁不可用,则立即返回false
,当前线程不会被阻塞。boolean tryLock(long time, TimeUnit unit)
: 尝试在给定的时间内获取锁。如果锁在指定时间内变得可用,则返回true
;如果锁在指定时间内始终不可用,则返回false
。在等待过程中,当前线程可以被中断。void unlock()
: 释放锁。在调用此方法之前,当前线程必须持有此锁。Lock
提供了比synchronized
更多的灵活性,如尝试非阻塞地获取锁、可中断地获取锁等。Lock
可以绑定多个Condition
对象,每个Condition
对象都管理着一个等待/通知队列,这使得能够更精细地控制线程的等待/唤醒行为。Lock
实现都是可重入的,即同一个线程可以多次获得同一个锁。Condition
接口是Lock
接口的一个补充,它提供了比Object
监视器方法(wait()
、notify()
、notifyAll()
)更强大的线程间通信能力。每个Lock
对象都可以关联多个Condition
对象,这允许线程以不同的条件等待和唤醒,从而实现更为复杂的同步控制逻辑。
void await()
: 使当前线程等待直到另一个线程调用该条件的signal()
或signalAll()
方法。在调用此方法之前,当前线程必须持有相关的锁。void awaitUninterruptibly()
: 与await()
类似,但该方法对中断不敏感,即线程在等待过程中不会被中断。long awaitNanos(long nanosTimeout)
: 使当前线程等待直到另一个线程调用该条件的signal()
或signalAll()
方法,或者直到等待时间超过指定的纳秒数。boolean awaitUntil(Date deadline)
: 使当前线程等待直到另一个线程调用该条件的signal()
或signalAll()
方法,或者直到给定的截止时间到达。void signal()
: 唤醒等待在该条件上的一个线程(如果存在)。在调用此方法之前,当前线程必须持有相关的锁。void signalAll()
: 唤醒等待在该条件上的所有线程。在调用此方法之前,当前线程必须持有相关的锁。Object
监视器方法的优势Lock
可以有多个Condition
实例,每个实例都管理着自己的等待队列,而Object
监视器方法只有一个等待队列。Condition
对象,可以基于不同的条件进行等待和唤醒,而Object
监视器方法只能进行无差别的唤醒。Condition
与Lock
紧密关联,因此可以在不释放锁的情况下进行等待,从而避免了在wait()
和notify()
调用之间的重新获取锁的开销和潜在的竞争条件。生产者-消费者问题是并发编程中的一个经典问题,用于演示线程间如何安全地共享和交换数据。利用Lock
和Condition
,我们可以更灵活地解决这个问题。
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumerDemo {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity;
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
public ProducerConsumerDemo(int capacity) {
this.capacity = capacity;
}
// 生产者方法
public void produce(int value) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 队列满时等待
}
queue.add(value);
System.out.println("Produced: " + value);
notEmpty.signal(); // 通知消费者
} finally {
lock.unlock();
}
}
// 消费者方法
public int consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 队列空时等待
}
int value = queue.poll();
System.out.println("Consumed: " + value);
notFull.signal(); // 通知生产者
return value;
} finally {
lock.unlock();
}
}
// 主函数,用于演示
public static void main(String[] args) {
ProducerConsumerDemo demo = new ProducerConsumerDemo(10);
// 启动生产者线程
new Thread(() -> {
for (int i = 0; i < 20; i++) {
try {
demo.produce(i);
Thread.sleep(100); // 模拟生产耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
// 启动消费者线程
new Thread(() -> {
for (int i = 0; i < 20; i++) {
try {
demo.consume();
Thread.sleep(200); // 模拟消费耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
}
在上述示例中,我们使用了ReentrantLock
作为锁实现,并通过它创建了两个Condition
对象:notEmpty
和notFull
,分别用于控制生产者在队列满时等待、消费者在队列空时等待,以及通知对方继续执行。这种实现方式比使用单个synchronized
块和wait()
/notify()
方法更为清晰和灵活。
Lock
和Condition
作为Java并发包中隐藏的管程实现,为并发编程提供了强大而灵活的工具。通过显式地获取和释放锁,以及基于不同条件的等待/唤醒机制,它们能够支持更为复杂的同步控制逻辑。在实际应用中,合理利用Lock
和Condition
,可以显著提升并发程序的性能和可靠性。然而,也需要注意,过度的锁使用和不当的同步控制,可能会导致死锁、活锁等并发问题,因此在使用时务必谨慎。