网络公司网站建设外包公司和劳务派遣的区别
文章目录
- 传统解法
- 解法一:传统解法 (`synchronized` + `wait/notifyAll`) 的流程解说
- 核心机制
- 生产者流程 (`produce`)
- 消费者流程 (`consume`)
- 关键点与缺点
- 多条件解法
- 解法二:多条件解法 (`ReentrantLock` + `Condition`) 的流程解说
- 核心机制
- 生产者流程 (`produce`)
- 消费者流程 (`consume`)
- 关键优势:精确唤醒
传统解法
package JUC_练习题.生产者消费者.传统解法;import java.util.LinkedList;
import java.util.Queue;public class ProducerConsumerSynchronized {// 共享的缓冲区static class Buffer {private final Queue<Integer> queue = new LinkedList<>();private final int capacity;public Buffer(int capacity) {this.capacity = capacity;}// 生产方法public synchronized void produce(int item) throws InterruptedException {// 如果缓冲区满了,生产者等待while (queue.size() == capacity) {System.out.println(Thread.currentThread().getName() + " 缓冲区满了,等待消费...");wait(); // 释放锁并等待}// 生产物品queue.offer(item);System.out.println(Thread.currentThread().getName() + " 生产了: " + item +",当前数量: " + queue.size());// 通知消费者可以消费了notifyAll();}// 消费方法public synchronized int consume() throws InterruptedException {// 如果缓冲区空了,消费者等待while (queue.isEmpty()) {System.out.println(Thread.currentThread().getName() + " 缓冲区空了,等待生产...");wait(); // 释放锁并等待}// 消费物品int item = queue.poll();System.out.println(Thread.currentThread().getName() + " 消费了: " + item +",剩余数量: " + queue.size());// 通知生产者可以生产了notifyAll();return item;}}public static void main(String[] args) {Buffer buffer = new Buffer(5); // 容量为5的缓冲区// 创建2个生产者for (int i = 1; i <= 2; i++) {new Thread(() -> {try {for (int j = 1; j <= 5; j++) {buffer.produce(j);Thread.sleep(100); // 模拟生产时间}} catch (InterruptedException e) {e.printStackTrace();}}, "生产者" + i).start();}// 创建3个消费者for (int i = 1; i <= 3; i++) {new Thread(() -> {try {for (int j = 1; j <= 3; j++) {buffer.consume();Thread.sleep(200); // 模拟消费时间}} catch (InterruptedException e) {e.printStackTrace();}}, "消费者" + i).start();}}
}
好的,我们来分别对这两个经典的生产者-消费者模型的解法进行详细的流程解说。
解法一:传统解法 (synchronized
+ wait/notifyAll
) 的流程解说
这个方案是Java最基础的线程同步与通信方式,它依赖于每个对象都拥有的“监视器锁”。
核心机制
- 锁:
Buffer
对象本身。当一个线程进入任何一个synchronized
方法(produce
或consume
)时,它就获得了buffer
对象的锁。 - 等待队列:每个锁对象(这里是
buffer
)只有一个等待队列(Wait Set)。所有调用wait()
的线程,不管是生产者还是消费者,都会进入这同一个队列里等待。 - 通信:通过
wait()
释放锁并等待,通过notifyAll()
唤醒等待队列中的所有线程。
生产者流程 (produce
)
- 一个生产者线程(比如“生产者1”)调用
buffer.produce()
。 - 它尝试获取
buffer
对象的锁。如果锁空闲,它就成功获得锁。此时,其他任何线程(包括其他生产者和所有消费者)都无法进入produce
或consume
方法。 - 它进入
while (queue.size() == capacity)
循环检查条件。- 如果缓冲区满了:
- 打印“缓冲区满了,等待消费…”。
- 调用
wait()
。此时,该生产者线程会立即释放它持有的buffer
锁,并进入buffer
对象的等待队列中休眠。
- 如果缓冲区未满:
- 跳出
while
循环。 - 向
queue
中添加一个物品 (queue.offer(item)
)。 - 打印生产信息。
- 调用
notifyAll()
。这个动作会唤醒所有正在buffer
等待队列中休眠的线程(包括可能在等待的其他生产者和所有消费者)。
- 跳出
- 如果缓冲区满了:
produce
方法执行完毕,线程退出synchronized
方法,正常释放锁。
消费者流程 (consume
)
- 一个消费者线程(比如“消费者1”)调用
buffer.consume()
。 - 它获取
buffer
对象的锁。 - 它进入
while (queue.isEmpty())
循环检查条件。- 如果缓冲区是空的:
- 打印“缓冲区空了,等待生产…”。
- 调用
wait()
,释放buffer
锁并进入等待队列。
- 如果缓冲区不空:
- 跳出
while
循环。 - 从
queue
中取出一个物品 (queue.poll()
)。 - 打印消费信息。
- 调用
notifyAll()
,唤醒所有在等待的线程。
- 跳出
- 如果缓冲区是空的:
consume
方法执行完毕,线程退出synchronized
方法,正常释放锁。
关键点与缺点
- 优点:实现简单,是Java内置的机制。
- 缺点:效率较低,存在“惊群效应” (Thundering Herd)。当一个生产者调用
notifyAll()
时,它会唤醒所有线程。但此时缓冲区可能只多了一个位置,那么被唤醒的其他生产者线程会发现条件依然不满足(还是满的),于是它们白白醒来一次,检查完条件后又得继续wait()
。这造成了不必要的线程上下文切换和CPU资源浪费。
多条件解法
package JUC_练习题.生产者消费者.可重入锁和多条件;import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;public class ProducerConsumerReentrantLock {// 共享的缓冲区static class Buffer {private final Queue<Integer> queue = new LinkedList<>();private final int capacity;// ReentrantLock 和两个条件变量private final ReentrantLock lock = new ReentrantLock();private final Condition notFull = lock.newCondition(); // 缓冲区不满的条件private final Condition notEmpty = lock.newCondition(); // 缓冲区不空的条件public Buffer(int capacity) {this.capacity = capacity;}// 生产方法public void produce(int item) throws InterruptedException {lock.lock(); // 获取锁try {// 如果缓冲区满了,在 notFull 条件上等待while (queue.size() == capacity) {System.out.println(Thread.currentThread().getName() +" 缓冲区满了,在notFull条件上等待...");notFull.await(); // 释放锁并等待}// 生产物品queue.offer(item);System.out.println(Thread.currentThread().getName() + " 生产了: " + item +",当前数量: " + queue.size());// 通知在 notEmpty 条件上等待的消费者notEmpty.signal(); // 精确通知消费者} finally {lock.unlock(); // 释放锁}}// 消费方法public int consume() throws InterruptedException {lock.lock(); // 获取锁try {// 如果缓冲区空了,在 notEmpty 条件上等待while (queue.isEmpty()) {System.out.println(Thread.currentThread().getName() +" 缓冲区空了,在notEmpty条件上等待...");notEmpty.await(); // 释放锁并等待}// 消费物品int item = queue.poll();System.out.println(Thread.currentThread().getName() + " 消费了: " + item +",剩余数量: " + queue.size());// 通知在 notFull 条件上等待的生产者notFull.signal(); // 精确通知生产者return item;} finally {lock.unlock(); // 释放锁}}}public static void main(String[] args) {Buffer buffer = new Buffer(5); // 容量为5的缓冲区// 创建2个生产者for (int i = 1; i <= 2; i++) {new Thread(() -> {try {for (int j = 1; j <= 5; j++) {buffer.produce(j);Thread.sleep(100); // 模拟生产时间}} catch (InterruptedException e) {e.printStackTrace();}}, "生产者" + i).start();}// 创建3个消费者for (int i = 1; i <= 3; i++) {new Thread(() -> {try {for (int j = 1; j <= 3; j++) {buffer.consume();Thread.sleep(200); // 模拟消费时间}} catch (InterruptedException e) {e.printStackTrace();}}, "消费者" + i).start();}}
}
解法二:多条件解法 (ReentrantLock
+ Condition
) 的流程解说
这个方案是JUC(java.util.concurrent
)包提供的更现代、更灵活的解决方案。
核心机制
- 锁:一个显式的
ReentrantLock
对象。通过lock.lock()
和lock.unlock()
来控制。 - 等待队列:不再是单一的队列。我们从一个
Lock
对象可以创建出多个Condition
对象,每个Condition
对象都拥有自己独立的等待队列。notFull
条件:管理所有因“缓冲区已满”而等待的生产者线程。notEmpty
条件:管理所有因“缓冲区为空”而等待的消费者线程。
- 通信:
await()
相当于wait()
,signal()
相当于notify()
。因为我们有两个独立的条件队列,所以可以实现精确唤醒。
生产者流程 (produce
)
-
生产者线程调用
produce()
。 -
它调用
lock.lock()
获取锁。 -
进入
try...finally
块(确保锁一定会被释放)。 -
检查
while (queue.size() == capacity)
条件。- 如果缓冲区满了:
- 打印“在notFull条件上等待…”。
- 调用
notFull.await()
。生产者线程会释放lock
锁,并进入notFull
自己的等待队列中休眠。
- 如果缓冲区未满:
- 生产物品。
- 打印信息。
- 关键一步:调用
notEmpty.signal()
。它只会唤醒一个正在notEmpty
条件队列中等待的线程(也就是一个消费者),而绝对不会去打扰任何在notFull
队列里等待的其他生产者。
- 如果缓冲区满了:
-
finally
块中的lock.unlock()
被执行,释放锁。
消费者流程 (consume
)
-
消费者线程调用
consume()
。 -
调用
lock.lock()
获取锁。 -
检查
while (queue.isEmpty())
条件。- 如果缓冲区是空的:
- 打印“在notEmpty条件上等待…”。
- 调用
notEmpty.await()
。消费者线程会释放lock
锁,并进入notEmpty
自己的等待队列中休眠。
- 如果缓冲区不空:
- 消费物品。
- 打印信息。
- 关键一步:调用
notFull.signal()
。它只会唤醒一个正在notFull
条件队列中等待的线程(也就是一个生产者)。
- 如果缓冲区是空的:
-
finally
块中的lock.unlock()
被执行,释放锁。
关键优势:精确唤醒
- 效率高:
ReentrantLock
+Condition
的方案通过分离等待队列,实现了精确唤醒。生产者只唤醒消费者,消费者只唤醒生产者。这完全避免了“惊群效应”,使得线程调度非常高效。 - 逻辑清晰:代码的意图更明确,
notFull.await()
就是在等“不满”的条件,notEmpty.signal()
就是在通知“不空”这个消息。 - 功能更强:
ReentrantLock
和Condition
提供了更丰富的功能,如可中断的等待、定时的等待、公平锁等。
总而言之,多条件解法是传统解法的一个全面升级,它通过更精细的控制,解决了传统解法的效率瓶颈。