Java并发基础:BlockingQueue和BlockingDeque接口的区别?

Java并发基础:BlockingQueue和BlockingDeque接口的区别?_第1张图片

核心概念

BlockingQueueBlockingDeque 它们都支持在并发编程中的线程安全操作,但是,这两个接口之间存在一些关键的区别,主要在于它们所支持的操作和数据结构的特性,如下:

1、数据结构特性

  1. BlockingQueue 是一个支持线程安全的队列,即它遵循 FIFO(先进先出)原则,可以向队列的尾部添加元素,并从队列的头部移除元素。
  2. BlockingDeque 是一个支持线程安全的双端队列(Deque,也称为双头队列),因此,可以在队列的头部和尾部添加或移除元素,BlockingDeque 提供了比 BlockingQueue 更多的操作灵活性。

2、操作

  1. BlockingQueue 提供了基本的队列操作,如 add(), offer(), put(), take(), poll() 等,这些方法主要用于在队列的尾部添加元素和从队列的头部移除元素。
  2. BlockingDeque 除了提供与 BlockingQueue 类似的操作外(但通常是以不同的名称提供,例如 offerFirst(), offerLast(), takeFirst(), takeLast() 等),还支持在队列的头部进行添加和移除操作的方法,如 offerFirst(), pollFirst(), peekFirst() 等,此外,它还提供了 push(), pop() 等栈操作,因为双端队列可以模拟栈的行为。

3、使用场景

  1. 当需要一个简单的、线程安全的 FIFO 队列时,BlockingQueue 是一个很好的选择,它通常用于生产者-消费者场景,其中生产者将数据放入队列,消费者从队列中取出数据。
  2. 当需要更多的灵活性,例如在队列的头部和尾部都能添加或移除元素时,BlockingDeque 是一个更好的选择,这种数据结构在需要同时维护队列和栈行为的场景中特别有用。

4、实现类

  1. BlockingQueue 提供了多种实现,如 ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue 等。
  2. BlockingDeque,常见的实现是 LinkedBlockingDeque,这个实现提供了一个基于链表的双端队列,支持在队列的两端进行高效的插入和移除操作。

代码案例

Java并发基础:BlockingQueue和BlockingDeque接口的区别?_第2张图片

BlockingQueue

BlockingQueue 接口表示一个可以存取元素,并且线程安全的队列,换句话说,多个线程可以同时从这个队列中安全地插入或者移除元素,BlockingQueue 的特性在于它支持在队列为空时,获取元素的线程将会等待,直到有元素可获取;当队列已满时,试图插入元素的线程将会等待,直到队列中有可用的空间,它的主要功能如下:

  1. 线程安全BlockingQueue 的所有操作都是线程安全的,这意味着在多线程环境中,可以安全地添加或移除元素,而不需要额外的同步措施。
  2. 阻塞操作:当队列为空时,从队列中获取元素的线程将会被阻塞,直到其他线程向队列中插入元素,同样,当队列已满时,试图插入元素的线程也会被阻塞,直到队列中有空间可用。
  3. 支持限时等待:除了无限期的等待外,BlockingQueue 还提供了带有超时参数的方法,允许线程在指定的时间内等待元素的插入或移除。
  4. 容量可选BlockingQueue 的实现类可以选择有界或无界,有界队列有一个固定的容量,而无界队列的容量则只受限于可用内存。

它有以下使用场景:

  1. 生产者-消费者模式:这是 BlockingQueue 最常见的使用场景,在这种模式中,生产者线程生成数据并将其放入队列,而消费者线程从队列中取出数据并处理,BlockingQueue 简化了生产者-消费者模式的实现,因为它负责处理线程间的同步和通信。
  2. 任务调度BlockingQueue 也可以用于任务调度系统中,在这种情况下,可以将待处理的任务作为元素添加到队列中,然后由工作线程从队列中取出任务并处理。
  3. 缓冲:在需要缓冲数据流或事件流的系统中,BlockingQueue 可以作为缓冲区使用,它允许数据或事件的生产者和消费者以不同的速率运行,而不会丢失数据或造成拥塞。
  4. 线程池:在 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(双端队列)的双端操作能力,从而提供了一个线程安全的、支持在两端添加和移除元素的阻塞队列,它有以下主要功能:

  1. 线程安全BlockingDeque的所有操作都是线程安全的,因此多个线程可以安全地同时访问它。
  2. 阻塞操作:如果队列为空,尝试从队列中取出元素的线程将会被阻塞,直到其他线程向队列中插入元素,同样,如果队列已满(对于有界队列),尝试插入元素的线程也会被阻塞,直到队列中有空间可用。
  3. 双端操作:与普通的Deque一样,BlockingDeque支持在队列的两端添加(addFirst, addLast, offerFirst, offerLast)和移除元素(removeFirst, removeLast, pollFirst, pollLast),此外,它还提供了检查队列两端元素的方法(getFirst, getLast, peekFirst, peekLast)。
  4. 容量可选BlockingDeque的实现类可以选择有界或无界,有界队列有一个固定的容量,而无界队列的容量则只受限于可用内存。

它有以下使用场景:

  1. 生产者-消费者模式:这是BlockingDeque最常见的使用场景之一,生产者线程在队列的一端添加元素,而消费者线程在另一端移除元素,这种模式特别适用于需要缓冲数据流或任务队列的系统。
  2. 工作窃取算法:在并行计算中,BlockingDeque可以用作工作窃取队列,实现工作线程之间的任务分配,当一个线程完成了自己的任务时,它可以从其他线程的任务队列中“窃取”任务来执行。
  3. 双端操作的需求:任何需要在队列的两端进行添加和移除操作的并发场景都可以使用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,生产者线程在队列的头部插入整数,而消费者线程在队列的尾部移除整数,注意,当队列满时,生产者线程会被阻塞;同样,当队列空时,消费者线程也会被阻塞,此外,使用putFirsttakeLast方法来演示双端队列的特性,也可以使用putLasttakeFirst来改变插入和移除元素的顺序。

Java并发基础:BlockingQueue和BlockingDeque接口的区别?_第3张图片

END!

你可能感兴趣的:(java,开发语言)