在Java并发编程中,java.util.concurrent.locks
包提供了一系列高级锁和条件变量,这些工具可以用来实现更复杂的并发控制。Lock
接口和Condition
接口是这个包中的重要组成部分,它们可以看作是管程(Monitor)的一种实现方式。下面我将详细介绍Lock
和Condition
的基本用法及其与管程的关系。
1. Lock 接口
Lock
接口提供了比synchronized
关键字更灵活的锁操作。它允许显式地获取和释放锁,并且可以支持公平锁和非公平锁。此外,Lock
还支持尝试获取锁的操作,这在某些场景下非常有用。
1.1 Lock 接口的主要方法
void lock()
:获取锁。void unlock()
:释放锁。boolean tryLock()
:尝试获取锁,如果锁可用则立即获取,否则返回false
。boolean tryLock(long time, TimeUnit unit)
:尝试获取锁,在指定时间内等待锁。
2. Condition 接口
Condition
接口提供了等待和通知操作,类似于Object
类中的wait()
、notify()
和notifyAll()
方法。但是Condition
提供了更高级别的控制能力,例如可以选择性地唤醒某些线程。
2.1 Condition 接口的主要方法
void await()
:释放锁并等待,直到被其他线程唤醒。void signal()
:唤醒一个等待中的线程。void signalAll()
:唤醒所有等待中的线程。
3. Lock 和 Condition 的关系
Lock
和Condition
可以结合起来使用,以实现更精细的并发控制。通常,一个Lock
对象可以产生多个Condition
对象,每个Condition
对象都可以用来控制不同条件下的线程等待和唤醒。
4. 示例代码
下面通过一个简单的生产者-消费者模式的例子来展示如何使用Lock
和Condition
。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Buffer {
private final int capacity = 10;
private final Object[] items = new Object[capacity];
private int putIndex = 0;
private int takeIndex = 0;
private int count = 0;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public void put(Object item) {
lock.lock();
try {
while (count == capacity) {
notFull.await(); // 缓冲区已满,等待消费者消费
}
items[putIndex] = item;
if (++putIndex == capacity) {
putIndex = 0;
}
++count;
notEmpty.signal(); // 通知消费者可以消费了
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
public Object take() {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 缓冲区为空,等待生产者生产
}
Object item = items[takeIndex];
if (++takeIndex == capacity) {
takeIndex = 0;
}
--count;
notFull.signal(); // 通知生产者可以生产了
return item;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
return null;
}
}
public class ProducerConsumerDemo {
public static void main(String[] args) {
final Buffer buffer = new Buffer();
Thread producer = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100); // 模拟生产时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
buffer.put(new Object());
System.out.println("Produced: " + i);
}
});
Thread consumer = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(200); // 模拟消费时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Object item = buffer.take();
System.out.println("Consumed: " + i);
}
});
producer.start();
consumer.start();
try {
producer.join();
consumer.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
5. 代码解释
-
Buffer 类:
- 定义了一个固定大小的缓冲区
items
,用于存放生产者生产的元素。 put
方法用于生产者向缓冲区添加元素。当缓冲区已满时,线程调用notFull.await()
方法进入等待状态,直到收到notEmpty.signal()
信号。take
方法用于消费者从缓冲区取出元素。当缓冲区为空时,线程调用notEmpty.await()
方法进入等待状态,直到收到notFull.signal()
信号。lock
对象用于保证线程安全,确保任何时候只有一个线程可以访问put
或take
方法。notFull
和notEmpty
是两个Condition
对象,分别用于控制生产者和消费者的等待和唤醒。
- 定义了一个固定大小的缓冲区
-
ProducerConsumerDemo 类:
- 创建了一个
Buffer
实例。 - 启动了一个生产者线程和一个消费者线程。
- 生产者线程每100毫秒生产一个元素,消费者线程每200毫秒消费一个元素。
- 使用
join()
方法等待两个线程完成。
- 创建了一个
6. 与管程的关系
在上述示例中,Buffer
类可以看作是一个管程。它封装了共享数据(items
数组)和一组操作这些数据的方法(put
和take
)。Lock
和Condition
的结合使用实现了互斥访问和条件变量的功能,这与管程的设计理念相吻合。
7. 总结
通过使用Lock
和Condition
,我们可以实现更高级别的并发控制,这在处理复杂的并发问题时非常有用。Lock
提供了比synchronized
更灵活的锁操作,而Condition
则提供了更精确的线程等待和唤醒机制。这些工具可以帮助我们更好地控制线程间的同步和通信,实现更高效的并发程序。
如果你有任何疑问或需要进一步的解释,请随时提问!