背景
在并发编程学习中,学习的时候认识到monitor中有owner、waitSet、entryList,了解到了entrylist是锁竞争失败后的等待队列,但是waitset还没有开始学习,不太清楚,于是搜索了下 waitset是什么,有什么用,和entrylist的区别,以下是整理的结果
Monitor里的waitSet和EntryList都是做什么的,两者之间的区别
在Java的并发编程中,Monitor(监视器)是实现同步和线程间通信的核心机制之一。Monitor 内部有两个重要的队列:等待集(Wait Set) 和 入口列表(Entry List) 。这两个队列分别用于不同的目的,理解它们的区别对于正确使用 synchronized 关键字以及 wait()、notify() 和 notifyAll() 方法非常重要。
1. 等待集(Wait Set)
定义:
- 等待集(Wait Set) 是一个包含所有调用
wait()方法后被挂起的线程的集合。当线程调用wait()时,它会释放当前持有的锁,并进入Wait Set,暂时挂起自己,直到另一个线程调用notify()或notifyAll()方法来唤醒它。
工作原理:
-
线程进入
Wait Set:- 当线程调用
wait()时,它会释放当前持有的锁,并进入Wait Set,暂时挂起自己。此时,线程不会参与对锁的竞争,也不会消耗CPU资源。
- 当线程调用
-
线程被唤醒:
- 当另一个线程调用
notify()或notifyAll()时,Monitor会选择一个或多个等待的线程从Wait Set中移除,并将它们标记为可运行状态。被唤醒的线程会重新竞争锁,只有成功获取锁的线程才能继续执行。
- 当另一个线程调用
-
线程重新获取锁:
- 被唤醒的线程在重新获取锁之前不会继续执行。这是因为
wait()方法在返回时会自动重新获取锁,以确保线程在进入临界区时仍然持有锁。
- 被唤醒的线程在重新获取锁之前不会继续执行。这是因为
使用场景:
- 条件等待 :线程可以通过
wait()方法进入Wait Set,并在满足某个条件之前暂停执行。这通常用于生产者-消费者模式、读写锁等场景,其中线程需要等待某些资源或条件发生变化。 - 线程协作 :通过
notify()和notifyAll(),线程可以通知其他等待的线程继续执行。这使得多个线程可以协调工作,确保它们在适当的时间点进行交互。
示例:
java
synchronized (lock) {
while (!condition) {
lock.wait(); // 线程进入 Wait Set,等待条件满足
}
// 条件满足后继续执行
}
2. 入口列表(Entry List)
定义:
- 入口列表(Entry List) 是一个包含所有试图获取锁但未能成功获取的线程的集合。当多个线程尝试进入同一个
synchronized块或方法时,只有一个线程能够成功获取锁并进入临界区,其他线程会被阻塞并放入Entry List,等待锁的释放。
工作原理:
-
线程进入
Entry List:- 当多个线程同时尝试获取同一个对象的锁时,只有一个线程能够成功获取锁并进入临界区,其他线程会被阻塞并放入
Entry List。这些线程会在Entry List中排队,等待锁的释放。
- 当多个线程同时尝试获取同一个对象的锁时,只有一个线程能够成功获取锁并进入临界区,其他线程会被阻塞并放入
-
线程获取锁:
- 当持有锁的线程退出
synchronized块或方法时,它会释放锁。此时,Monitor会从Entry List中选择一个线程,允许它获取锁并继续执行。选择的顺序通常是FIFO(先进先出),但JVM的具体实现可能会有所不同。
- 当持有锁的线程退出
-
线程竞争锁:
- 如果有多个线程在
Entry List中等待,Monitor会根据某种调度算法(如FIFO或优先级调度)选择一个线程来获取锁。被选中的线程会从Entry List中移除,并尝试获取锁。如果获取成功,该线程可以进入临界区;否则,它会继续等待。
- 如果有多个线程在
使用场景:
- 锁竞争 :当多个线程尝试进入同一个
synchronized块或方法时,Entry List用于管理这些线程的排队和锁的分配。它确保在同一时间只有一个线程能够持有锁并进入临界区,从而避免数据竞争和不一致的问题。 - 线程调度 :
Entry List还负责管理线程的调度,确保线程在适当的时机获取锁并继续执行。
示例:
java
synchronized (lock) {
// 只有一个线程能够进入这个临界区,其他线程会被放入 Entry List
// 等待锁的释放
}
3. Wait Set 和 Entry List 的区别
| 特性 | 等待集(Wait Set) | 入口列表(Entry List) |
|---|---|---|
| 定义 | 包含所有调用 wait() 后被挂起的线程 |
包含所有试图获取锁但未能成功获取的线程 |
| 线程状态 | 线程已经获取了锁,但在 wait() 后释放了锁并进入等待状态 |
线程尚未获取锁,正在等待锁的释放 |
| 进入条件 | 线程调用 wait() 后进入 Wait Set |
线程尝试获取锁但未能成功时进入 Entry List |
| 唤醒方式 | 通过 notify() 或 notifyAll() 唤醒 |
通过锁的释放(即持有锁的线程退出 synchronized 块)唤醒 |
| 重新获取锁 | 被唤醒的线程需要重新竞争锁 | 线程从 Entry List 中被选中后直接获取锁 |
| 典型使用场景 | 条件等待、线程协作 | 锁竞争、线程调度 |
| 示例方法 | wait()、notify()、notifyAll() |
synchronized 块或方法 |
4. 示例:Wait Set 和 Entry List 的结合使用
以下是一个简单的生产者-消费者问题的示例,展示了 Wait Set 和 Entry List 的结合使用:
java
public class ProducerConsumerExample {
private static final Object lock = new Object();
private static final Queue<Integer> buffer = new LinkedList<>();
private static final int MAX_SIZE = 10;
public static void main(String[] args) {
Thread producer = new Thread(() -> {
for (int i = 0; i < 20; i++) {
produce(i);
}
});
Thread consumer = new Thread(() -> {
for (int i = 0; i < 20; i++) {
consume();
}
});
producer.start();
consumer.start();
}
private static void produce(int value) {
synchronized (lock) {
// 如果缓冲区已满,生产者进入 Wait Set,等待消费者消费
while (buffer.size() == MAX_SIZE) {
try {
System.out.println("Buffer is full, producer is waiting...");
lock.wait(); // 生产者进入 Wait Set
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
buffer.add(value);
System.out.println("Produced: " + value);
lock.notify(); // 唤醒一个等待的消费者
}
}
private static void consume() {
synchronized (lock) {
// 如果缓冲区为空,消费者进入 Wait Set,等待生产者生产
while (buffer.isEmpty()) {
try {
System.out.println("Buffer is empty, consumer is waiting...");
lock.wait(); // 消费者进入 Wait Set
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
int value = buffer.remove();
System.out.println("Consumed: " + value);
lock.notify(); // 唤醒一个等待的生产者
}
}
}
在这个示例中:
Wait Set:生产者和消费者通过wait()和notify()在Wait Set中协作。生产者在缓冲区满时进入Wait Set,等待消费者消费;消费者在缓冲区空时进入Wait Set,等待生产者生产。Entry List:当多个生产者或消费者尝试进入synchronized块时,未能获取锁的线程会被放入Entry List,等待锁的释放。只有成功获取锁的线程才能进入临界区进行生产和消费操作。
5. 总结
Wait Set:用于线程的条件等待和协作。线程在调用wait()后进入Wait Set,并在满足条件时通过notify()或notifyAll()被唤醒。被唤醒的线程需要重新竞争锁。Entry List:用于管理锁的竞争。当多个线程尝试获取同一个锁时,未能成功获取锁的线程会被放入Entry List,等待锁的释放。锁释放后,Monitor会从Entry List中选择一个线程来获取锁并继续执行。
理解 Wait Set 和 Entry List 的区别和作用,可以帮助你更好地设计和实现高效的多线程程序,特别是在需要线程协作和锁管理的场景中。
waitSet详解
在Java的并发编程中,Monitor(监视器)是实现同步和线程间通信的核心机制之一。Monitor不仅负责管理锁的状态,还提供了一种线程间的协作机制,即等待集(Wait Set) 。Wait Set是Monitor中的一个重要组成部分,用于支持线程之间的条件等待和通知操作。
1. 什么是 Wait Set?
Wait Set 是一个包含所有正在等待某个特定条件的线程的集合。当一个线程调用对象的 wait() 方法时,该线程会释放当前持有的锁,并进入该对象的 Wait Set,暂时挂起自己,直到另一个线程调用 notify() 或 notifyAll() 方法来唤醒它。
wait():线程调用wait()后,会释放当前持有的锁,并进入Wait Set,等待其他线程的通知。notify():线程调用notify()后,会从Wait Set中随机选择一个等待的线程并唤醒它。notifyAll():线程调用notifyAll()后,会唤醒Wait Set中所有等待的线程。
2. Wait Set 的作用
Wait Set 的主要作用是允许线程在满足某些条件之前暂时挂起自己,避免不必要的忙等待(Busy Waiting),从而提高系统的效率和响应性。具体来说,Wait Set 有以下几个重要的功能:
2.1 条件等待
线程可以通过 wait() 方法进入 Wait Set,并在满足某个条件之前暂停执行。这通常用于生产者-消费者模式、读写锁等场景,其中线程需要等待某些资源或条件发生变化。
例如,在生产者-消费者问题中,消费者线程可能会因为缓冲区为空而调用 wait(),进入 Wait Set,等待生产者线程将数据放入缓冲区后通过 notify() 唤醒它。
java
synchronized (buffer) {
while (buffer.isEmpty()) {
buffer.wait(); // 消费者线程进入 Wait Set,等待生产者通知
}
// 继续消费数据
}
2.2 线程协作
Wait Set 提供了线程之间的协作机制。通过 notify() 和 notifyAll(),线程可以通知其他等待的线程继续执行。这使得多个线程可以协调工作,确保它们在适当的时间点进行交互。
notify():唤醒Wait Set中的一个随机线程。适用于只需要唤醒一个线程的情况。notifyAll():唤醒Wait Set中的所有线程。适用于需要唤醒多个线程的情况,或者不确定哪个线程应该被唤醒。
java
synchronized (buffer) {
buffer.notify(); // 唤醒一个等待的消费者线程
}
2.3 避免忙等待
如果没有 Wait Set,线程可能会使用忙等待(Busy Waiting)的方式不断检查条件是否满足。这种方式会浪费大量的CPU资源,降低系统的性能。Wait Set 通过让线程进入休眠状态,避免了不必要的CPU占用,提高了系统的效率。
java
// 不推荐的忙等待方式
while (!condition) {
// 一直循环检查条件,浪费CPU资源
}
// 推荐的使用 wait() 和 notify() 方式
synchronized (lock) {
while (!condition) {
lock.wait(); // 线程进入 Wait Set,等待条件满足
}
// 条件满足后继续执行
}
3. Wait Set 的工作原理
Wait Set 的工作原理如下:
- 线程进入
Wait Set:- 当线程调用
wait()时,它会释放当前持有的锁,并进入Wait Set,暂时挂起自己。此时,线程不会参与对锁的竞争,也不会消耗CPU资源。
- 当线程调用
- 线程被唤醒 :
- 当另一个线程调用
notify()或notifyAll()时,Monitor会选择一个或多个等待的线程从Wait Set中移除,并将它们标记为可运行状态。被唤醒的线程会重新竞争锁,只有成功获取锁的线程才能继续执行。
- 当另一个线程调用
- 线程重新获取锁 :
- 被唤醒的线程在重新获取锁之前不会继续执行。这是因为
wait()方法在返回时会自动重新获取锁,以确保线程在进入临界区时仍然持有锁。
- 被唤醒的线程在重新获取锁之前不会继续执行。这是因为
4. 注意事项
- 必须在
synchronized块中调用wait()、notify()和notifyAll():- 这些方法只能在已经获取了对象锁的情况下调用。如果在没有同步块的情况下调用这些方法,会抛出
IllegalMonitorStateException异常。
- 这些方法只能在已经获取了对象锁的情况下调用。如果在没有同步块的情况下调用这些方法,会抛出
wait()应该放在while循环中 :- 由于
wait()可能会被虚假唤醒(Spurious Wakeup),因此建议在while循环中使用wait(),以确保线程只在条件真正满足时才继续执行。
- 由于
java
synchronized (lock) {
while (!condition) {
lock.wait();
}
// 条件满足后继续执行
}
notify()和notifyAll()的选择 :- 如果只需要唤醒一个线程,可以使用
notify()。如果不确定哪个线程应该被唤醒,或者需要唤醒多个线程,可以使用notifyAll()。使用notifyAll()可以避免潜在的死锁问题,但可能会导致更多的线程竞争锁。
- 如果只需要唤醒一个线程,可以使用
5. 示例:生产者-消费者问题
以下是一个简单的生产者-消费者问题的示例,展示了 Wait Set 的使用:
java
public class ProducerConsumerExample {
private static final Object lock = new Object();
private static final Queue<Integer> buffer = new LinkedList<>();
private static final int MAX_SIZE = 10;
public static void main(String[] args) {
Thread producer = new Thread(() -> {
for (int i = 0; i < 20; i++) {
produce(i);
}
});
Thread consumer = new Thread(() -> {
for (int i = 0; i < 20; i++) {
consume();
}
});
producer.start();
consumer.start();
}
private static void produce(int value) {
synchronized (lock) {
while (buffer.size() == MAX_SIZE) {
try {
System.out.println("Buffer is full, producer is waiting...");
lock.wait(); // 生产者进入 Wait Set,等待消费者消费
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
buffer.add(value);
System.out.println("Produced: " + value);
lock.notify(); // 唤醒一个等待的消费者
}
}
private static void consume() {
synchronized (lock) {
while (buffer.isEmpty()) {
try {
System.out.println("Buffer is empty, consumer is waiting...");
lock.wait(); // 消费者进入 Wait Set,等待生产者生产
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
int value = buffer.remove();
System.out.println("Consumed: " + value);
lock.notify(); // 唤醒一个等待的生产者
}
}
}
在这个示例中,生产者和消费者通过 wait() 和 notify() 在 Wait Set 中协作。生产者在缓冲区满时进入 Wait Set,等待消费者消费;消费者在缓冲区空时进入 Wait Set,等待生产者生产。通过这种方式,生产者和消费者可以高效地协同工作,避免忙等待和资源浪费。
6. 总结
Wait Set是Monitor中的一个重要组成部分,用于支持线程的条件等待和通知操作。- 它允许线程在满足某些条件之前暂时挂起自己,避免不必要的忙等待,从而提高系统的效率和响应性。
wait()、notify()和notifyAll()是Wait Set的核心操作,分别用于线程的等待、单个线程的唤醒和所有线程的唤醒。- 使用
Wait Set时需要注意,wait()必须在synchronized块中调用,并且通常应放在while循环中,以防止虚假唤醒。
理解 Wait Set 的工作原理和使用场景,可以帮助你更好地设计和实现高效的多线程程序,特别是在需要线程协作和条件等待的场景中。