目录
1.阻塞队列是什么
生产者-消费者模型
2.标准库中的阻塞队列
⽣产者-消费者模型
阻塞队列实现
阻塞队列(Blocking Queue)是一种特殊类型的队列,它在插入和删除元素时可以提供阻塞机制。阻塞队列能是⼀种线程安全的数据结构, 并且具有以下特性:
当队列满的时候, 继续⼊队列就会阻塞, 直到有其他线程从队列中取⾛元素.当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插⼊元素.
1. 阻塞队列就相当于⼀个缓冲区,平衡了⽣产者和消费者的处理能⼒. (削峰填⾕)
就像三峡大坝一样。它是为了抵御突然的洪水修建的。洪水突然来临的时候,三峡大坝可以缓解巨大的洪水量,将它储蓄起来。经过三峡大坝缓冲后,流到下流的水就不会特别大。当长时间干旱没有下雨的时候,三峡大坝就可以开闸放水,来填补。
2. 阻塞队列也能使⽣产者和消费者之间 解耦
• BlockingQueue 是⼀个接⼝. 真正实现的类是 LinkedBlockingQueue.• put ⽅法⽤于阻塞式的⼊队列, take ⽤于阻塞式的出队列.• BlockingQueue 也有 offer, poll, peek 等⽅法, 但是这些⽅法不带有阻塞特性.
BlockingQueue queue = new LinkedBlockingQueue<>();
// ⼊队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞.
String elem = queue.take();
生产者线程使用随机数生成器随机产生元素,并将其放入阻塞队列中。如果队列已满,则生产者线程会阻塞等待,直到有空闲位置可供插入。消费者线程从阻塞队列中获取元素,并进行相应的处理。如果队列为空,则消费者线程会阻塞等待,直到有数据可供消费。
通过使用阻塞队列,生产者和消费者之间实现了解耦,避免了线程之间的竞争和忙等待,提高了系统的效率和性能。同时,阻塞队列提供了线程安全的插入和移除操作,保证了数据的有序处理和线程间的协调。
在主函数中,启动了一个生产者线程和一个消费者线程,并使用 join() 方法等待它们的执行完成。这样可以确保程序在所有线程都执行完毕后退出,避免产生未处理的异常。
public static void main(String[] args) throws InterruptedException {
BlockingQueue blockingQueue = new LinkedBlockingQueue();
Thread customer = new Thread(() -> {
while (true) {
try {
int value = blockingQueue.take();
System.out.println("消费元素: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者");
customer.start();
Thread producer = new Thread(() -> {
Random random = new Random();
while (true) {
try {
int num = random.nextInt(1000);
System.out.println("⽣产元素: " + num);
blockingQueue.put(num);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "⽣产者");
producer.start();
customer.join();
producer.join();
}
通过 "循环队列" 的⽅式来实现.使⽤ synchronized 进⾏加锁控制.put 插⼊元素的时候, 判定如果队列满了, 就进⾏ wait. (注意, 要在循环中进⾏ wait. 被唤醒时不⼀定队列就不满了, 因为同时可能是唤醒了多个线程).take 取出元素的时候, 判定如果队列为空, 就进⾏ wait. (也是循环 wait)
首先MyBlockingQueue
使用一个字符串数组 elems
来存储元素,同时维护了头指针 head
、尾指针 tail
和队列中元素的个数 size
。通过 synchronized
关键字和一个锁对象 locker
,实现了线程安全的入队和出队操作。
再用put
方法用于向队列中添加元素。如果队列已满,则调用 locker.wait()
阻塞当前线程,直到有空闲位置可供插入。然后将元素放入队列尾部,并更新尾指针和队列大小。最后调用 locker.notify()
唤醒其他可能正在等待的线程。
take
方法用于从队列中取出元素。如果队列为空,则调用 locker.wait()
阻塞当前线程,直到有元素可供取出。然后取出头指针位置的元素,并更新头指针和队列大小。最后调用 locker.notify()
唤醒其他可能正在等待的线程,并返回取出的元素。
最后在主函数中,创建了一个容量为 1000 的 MyBlockingQueue
对象,并依次向队列中放入了四个元素。然后使用 take
方法依次取出队列中的元素,并打印输出。
package 多线程;
//为了简单,不写作泛型的形式,考虑存储的元素就是单纯的String
class MyBlockingQueue{
private String[] elems = null;
private int head = 0;
private int tail = 0;
private int size = 0;
//准备一个锁对象
private Object locker = new Object();
public MyBlockingQueue(int capacity){
elems = new String[capacity];
}
public void put(String elem) throws InterruptedException {
synchronized (locker){// 此处最好使⽤ while.
// 否则 notifyAll 的时候, 该线程从 wait 中被唤醒,
// 但是紧接着并未抢占到锁. 当锁被抢占的时候, 可能⼜已经队列满了
// 就只能继续等待
while (size >= elems.length){
//队列满了
locker.wait();
//后续需要让这个代码能够阻塞;
return;
}
//新的元素要放到tail 指向的位置上
elems[tail] = elem;
tail++;
if(tail >= elems.length){
tail = 0;
}
size++;
locker.notify();//入队列成功后唤醒
}
}//写操作
public String take() throws InterruptedException {
String elem = null;
synchronized (locker){
while (size == 0){
//队列空了
locker.wait();
// 后续也需要让这个代码阻塞
}
//取出 head 位置的元素并 返回
elem = elems[head];
head++;
if (head >= elems.length){
head = 0;
}
size--;
locker.notify();//出队列成功后唤醒
}
return elem;
}
}
public class ThreadDemo24 {
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue queue = new MyBlockingQueue(1000);
queue.put("aaa");
queue.put("bbb");
queue.put("ccc");
queue.put("vvv");
String elem = "";
elem = queue.take();
System.out.println("elem: " + elem);
elem = queue.take();
System.out.println("elem: " + elem);
elem = queue.take();
System.out.println("elem: " + elem);
elem = queue.take();
System.out.println("elem: " + elem);
}
}
运行结果