背景
在并发编程学习中,学习的时候认识到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
的工作原理和使用场景,可以帮助你更好地设计和实现高效的多线程程序,特别是在需要线程协作和条件等待的场景中。