核心概念
BlockingQueue
和 BlockingDeque
它们都支持在并发编程中的线程安全操作,但是,这两个接口之间存在一些关键的区别,主要在于它们所支持的操作和数据结构的特性,如下:
1、数据结构特性:
BlockingQueue
是一个支持线程安全的队列,即它遵循 FIFO(先进先出)原则,可以向队列的尾部添加元素,并从队列的头部移除元素。BlockingDeque
是一个支持线程安全的双端队列(Deque,也称为双头队列),因此,可以在队列的头部和尾部添加或移除元素,BlockingDeque
提供了比BlockingQueue
更多的操作灵活性。
2、操作:
BlockingQueue
提供了基本的队列操作,如add()
,offer()
,put()
,take()
,poll()
等,这些方法主要用于在队列的尾部添加元素和从队列的头部移除元素。BlockingDeque
除了提供与BlockingQueue
类似的操作外(但通常是以不同的名称提供,例如offerFirst()
,offerLast()
,takeFirst()
,takeLast()
等),还支持在队列的头部进行添加和移除操作的方法,如offerFirst()
,pollFirst()
,peekFirst()
等,此外,它还提供了push()
,pop()
等栈操作,因为双端队列可以模拟栈的行为。
3、使用场景:
- 当需要一个简单的、线程安全的 FIFO 队列时,
BlockingQueue
是一个很好的选择,它通常用于生产者-消费者场景,其中生产者将数据放入队列,消费者从队列中取出数据。 - 当需要更多的灵活性,例如在队列的头部和尾部都能添加或移除元素时,
BlockingDeque
是一个更好的选择,这种数据结构在需要同时维护队列和栈行为的场景中特别有用。
4、实现类:
BlockingQueue
提供了多种实现,如ArrayBlockingQueue
,LinkedBlockingQueue
,PriorityBlockingQueue
,SynchronousQueue
等。BlockingDeque
,常见的实现是LinkedBlockingDeque
,这个实现提供了一个基于链表的双端队列,支持在队列的两端进行高效的插入和移除操作。
代码案例
BlockingQueue
BlockingQueue
接口表示一个可以存取元素,并且线程安全的队列,换句话说,多个线程可以同时从这个队列中安全地插入或者移除元素,BlockingQueue
的特性在于它支持在队列为空时,获取元素的线程将会等待,直到有元素可获取;当队列已满时,试图插入元素的线程将会等待,直到队列中有可用的空间,它的主要功能如下:
- 线程安全:
BlockingQueue
的所有操作都是线程安全的,这意味着在多线程环境中,可以安全地添加或移除元素,而不需要额外的同步措施。 - 阻塞操作:当队列为空时,从队列中获取元素的线程将会被阻塞,直到其他线程向队列中插入元素,同样,当队列已满时,试图插入元素的线程也会被阻塞,直到队列中有空间可用。
- 支持限时等待:除了无限期的等待外,
BlockingQueue
还提供了带有超时参数的方法,允许线程在指定的时间内等待元素的插入或移除。 - 容量可选:
BlockingQueue
的实现类可以选择有界或无界,有界队列有一个固定的容量,而无界队列的容量则只受限于可用内存。
它有以下使用场景:
- 生产者-消费者模式:这是
BlockingQueue
最常见的使用场景,在这种模式中,生产者线程生成数据并将其放入队列,而消费者线程从队列中取出数据并处理,BlockingQueue
简化了生产者-消费者模式的实现,因为它负责处理线程间的同步和通信。 - 任务调度:
BlockingQueue
也可以用于任务调度系统中,在这种情况下,可以将待处理的任务作为元素添加到队列中,然后由工作线程从队列中取出任务并处理。 - 缓冲:在需要缓冲数据流或事件流的系统中,
BlockingQueue
可以作为缓冲区使用,它允许数据或事件的生产者和消费者以不同的速率运行,而不会丢失数据或造成拥塞。 - 线程池:在
ExecutorService
框架中,BlockingQueue
用于存储待执行的任务,线程池中的工作线程从队列中取出任务并执行。
下面是一个简单的生产者-消费者示例,展示了如何使用 BlockingQueue
,如下代码:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ProducerConsumerExample {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
// 生产者线程
Thread producer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println("生产者生产了 " + i);
queue.put(i); // 如果队列满了,将会阻塞
Thread.sleep(200); // 模拟生产时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 消费者线程
Thread consumer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
Integer item = queue.take(); // 如果队列为空,将会阻塞
System.out.println("消费者消费了 " + item);
Thread.sleep(500); // 模拟消费时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动线程
producer.start();
consumer.start();
}
}
BlockingDeque
BlockingDeque
结合了BlockingQueue
的阻塞特性和Deque
(双端队列)的双端操作能力,从而提供了一个线程安全的、支持在两端添加和移除元素的阻塞队列,它有以下主要功能:
- 线程安全:
BlockingDeque
的所有操作都是线程安全的,因此多个线程可以安全地同时访问它。 - 阻塞操作:如果队列为空,尝试从队列中取出元素的线程将会被阻塞,直到其他线程向队列中插入元素,同样,如果队列已满(对于有界队列),尝试插入元素的线程也会被阻塞,直到队列中有空间可用。
- 双端操作:与普通的
Deque
一样,BlockingDeque
支持在队列的两端添加(addFirst
,addLast
,offerFirst
,offerLast
)和移除元素(removeFirst
,removeLast
,pollFirst
,pollLast
),此外,它还提供了检查队列两端元素的方法(getFirst
,getLast
,peekFirst
,peekLast
)。 - 容量可选:
BlockingDeque
的实现类可以选择有界或无界,有界队列有一个固定的容量,而无界队列的容量则只受限于可用内存。
它有以下使用场景:
- 生产者-消费者模式:这是
BlockingDeque
最常见的使用场景之一,生产者线程在队列的一端添加元素,而消费者线程在另一端移除元素,这种模式特别适用于需要缓冲数据流或任务队列的系统。 - 工作窃取算法:在并行计算中,
BlockingDeque
可以用作工作窃取队列,实现工作线程之间的任务分配,当一个线程完成了自己的任务时,它可以从其他线程的任务队列中“窃取”任务来执行。 - 双端操作的需求:任何需要在队列的两端进行添加和移除操作的并发场景都可以使用
BlockingDeque
,例如,实现一个并发的LRU(最近最少使用)缓存时,可能需要使用BlockingDeque
来维护访问顺序。
下面是一个使用BlockingDeque
的简单生产者-消费者示例,如下代码:
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
public class ProducerConsumerWithBlockingDeque {
public static void main(String[] args) {
BlockingDeque<Integer> deque = new LinkedBlockingDeque<>(5);
// 生产者线程
Thread producer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println("生产者生产了 " + i);
deque.putFirst(i); // 在队列头部插入元素,如果队列满了,将会阻塞
Thread.sleep(200); // 模拟生产时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 消费者线程
Thread consumer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
Integer item = deque.takeLast(); // 从队列尾部移除元素,如果队列为空,将会阻塞
System.out.println("消费者消费了 " + item);
Thread.sleep(500); // 模拟消费时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动线程
producer.start();
consumer.start();
}
}
在这个示例中,创建了一个容量为5的LinkedBlockingDeque
,生产者线程在队列的头部插入整数,而消费者线程在队列的尾部移除整数,注意,当队列满时,生产者线程会被阻塞;同样,当队列空时,消费者线程也会被阻塞,此外,使用putFirst
和takeLast
方法来演示双端队列的特性,也可以使用putLast
和takeFirst
来改变插入和移除元素的顺序。
END!
往期回顾
Java并发基础:LinkedTransferQueue全面解析!
Java并发基础:BlockingQueue和BlockingDeque接口的区别?