Java进阶篇--并发容器之ArrayBlockingQueue与LinkedBlockingQueue

目录

ArrayBlockingQueue简介

ArrayBlockingQueue的主要属性

put方法

take方法

ArrayBlockingQueue代码示例

LinkedBlockingQueue简介

LinkedBlockingQueue的主要属性

put方法详解

take方法详解

LinkedBlockingQueue代码示例

ArrayBlockingQueue与LinkedBlockingQueue的比较


ArrayBlockingQueue简介

ArrayBlockingQueue 是一个基于数组的有界阻塞队列,是线程安全的。它的容量是固定的,创建时需要指定队列的大小。ArrayBlockingQueue 使用一个可重入锁来保证线程安全,并使用条件变量来实现在队列为空或队列已满时线程的阻塞与唤醒。

ArrayBlockingQueue 的特点如下:

  1. 有界性:它是一个固定容量的队列,创建时需要指定队列的大小,并且该大小不能改变。
  2. 先进先出:队列采用 FIFO(先进先出)的原则,即最先插入的元素将首先被移除。
  3. 阻塞操作:当队列已满时,插入操作将被阻塞,直到有空余位置;当队列为空时,移除操作将被阻塞,直到有元素可供移除。
  4. 线程安全:ArrayBlockingQueue 内部使用锁和条件变量来保证多线程环境下的线程安全性。

ArrayBlockingQueue 的实现原理如下:

  1. 内部数据结构:内部使用一个数组来存储队列元素。当元素被插入队尾时,会直接放置到数组的尾部;当元素被移除队首时,会从数组头部取出元素。同时,ArrayBlockingQueue 使用两个整型变量作为队列的头部和尾部指针来标识队列的状态。
  2. 线程安全:ArrayBlockingQueue 内部使用 ReentrantLock 来保证线程安全。获取锁失败的线程会进入等待队列,等待锁被释放后重新尝试获取锁。另外,ArrayBlockingQueue 在条件变量上阻塞线程的时候,也使用了 ReentrantLock 的 Condition 来实现。
  3. 队列空间的分配与释放:当元素被插入队列时,会计算出元素的位置,并将元素直接放置到数组中。当元素被移除队列时,会将队首元素返回,并将队首指针指向下一个元素。在队列中空出的位置可以被下一个插入操作使用,因此并不需要做额外的内存分配和释放操作。
  4. put 和 take 方法的实现:当队列已满时,插入操作将被阻塞,直到有空余位置;当队列为空时,移除操作将被阻塞,直到有元素可供移除。这是通过使用 ReentrantLock 的 Condition 实现的。

ArrayBlockingQueue 提供了以下常用方法:

  • add(E e): 将元素插入队尾,如果队列已满抛出异常。
  • offer(E e): 将元素插入队尾,如果队列已满返回 false。
  • put(E e): 将元素插入队尾,如果队列已满则阻塞直到可插入。
  • poll(): 移除并返回队首元素,如果队列为空则返回 null。
  • take(): 移除并返回队首元素,如果队列为空则阻塞直到有元素可供移除。
  • size(): 返回队列中当前元素的数量。

总之,ArrayBlockingQueue 是一种高效、线程安全的数据结构,适用于多线程环境下的生产者-消费者问题,其中生产者线程负责将元素插入队尾,消费者线程负责从队首取出元素进行处理。它还可以用于有界任务调度、有界缓冲区等场景,限制了队列中元素的数量。

ArrayBlockingQueue的主要属性

ArrayBlockingQueue的主要属性包括:

  • items:存储队列元素的数组。
  • takeIndex:下一个take、poll、peek或remove操作的索引。
  • putIndex:下一个put、offer或add操作的索引。
  • count:队列中元素的数量。
  • lock:控制并发访问的主锁。
  • notEmpty:等待获取元素的条件。
  • notFull:等待添加元素的条件。

ArrayBlockingQueue使用数组来实现数据存储。为了保证线程安全,它使用了ReentrantLock(lock)(看这篇文章的详细介绍)作为主要锁,并使用了Condition(notEmpty和notFull)来实现可阻塞式的插入和删除操作。

在构造方法中,会创建lock、notEmpty和notFull等属性。

具体来说,当队列已满时,put操作将线程放置到notFull等待队列中,直到队列有空位可以插入数据。而当队列为空时,take操作将线程放置到notEmpty等待队列中,直到队列有数据可以被消费。

put方法

put方法通过判断队列是否已满来决定是否阻塞线程,如果队列已满,则将线程放入notFull等待队列中;如果满足插入数据的条件,则进行数据插入并通知消费者线程。

其方法签名如下:

public void put(E element) throws InterruptedException

element:要插入队列的元素。
put 方法的步骤如下:

  1. 如果元素为 null,则抛出 NullPointerException 异常。
  2. 使用可重入锁获取锁。
  3. 如果队列已满,使用条件变量进行等待,直到有空闲位置。
  4. 将元素插入队列末尾,同时更新队列的大小和指针位置。
  5. 唤醒可能正在等待元素出队操作的线程。
  6. 释放锁。

如果在等待期间被中断,将抛出 InterruptedException 异常,并且会在抛出异常前恢复中断状态。

take方法

take方法通过判断队列是否为空来决定是否阻塞线程,如果队列为空,则将线程放入notEmpty等待队列中;如果队列不为空,则获取并移除数据,并通知生产者线程。

其方法签名如下:

public E take() throws InterruptedException

take 方法的步骤如下:

  1. 使用可重入锁获取锁。
  2. 如果队列为空,使用条件变量进行等待,直到有元素可用。
  3. 从队列头部取出元素,同时更新队列的大小和指针位置。
  4. 唤醒可能正在等待元素入队操作的线程。
  5. 释放锁。
  6. 返回取出的元素。

如果在等待期间被中断,将抛出 InterruptedException 异常,并且会在抛出异常前恢复中断状态。

通过以上分析,可以看出ArrayBlockingQueue通过Condition的通知机制实现了可阻塞式的插入和获取操作。

ArrayBlockingQueue代码示例

import java.util.concurrent.ArrayBlockingQueue;

public class main {
    private static final int BUFFER_SIZE = 10;
    private static final int NUM_PRODUCERS = 3;
    private static final int NUM_CONSUMERS = 2;

    public static void main(String[] args) {
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(BUFFER_SIZE);

        // 创建生产者线程
        for (int i = 0; i < NUM_PRODUCERS; i++) {
            Thread producer = new Thread(() -> {
                try {
                    int count = 1;
                    while (true) {
                        queue.put(count);
                        System.out.println(Thread.currentThread().getName() + " 生产: " + count);
                        count++;
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            producer.start();
        }

        // 创建消费者线程
        for (int i = 0; i < NUM_CONSUMERS; i++) {
            Thread consumer = new Thread(() -> {
                try {
                    while (true) {
                        int data = queue.take();
                        System.out.println(Thread.currentThread().getName() + " 已消耗: " + data);
                        Thread.sleep(2000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            consumer.start();
        }

        try {
            // 主线程等待一定时间后停止生产者和消费者线程
            Thread.sleep(10000);
            System.out.println("停止生产者和消费者...");
            // 中断所有生产者和消费者线程
            Thread.currentThread().getThreadGroup().interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,我们创建了3个生产者线程和2个消费者线程。每个生产者线程不断向队列中放入递增的数据,每个消费者线程不断从队列中取出数据进行消费。

主线程等待10秒后,通过中断方式停止所有生产者和消费者线程。在每个生产者和消费者线程的循环中,检查线程是否被中断,如果是则退出循环并终止线程。

这个示例展示了如何使用ArrayBlockingQueue实现多生产者和多消费者之间的协作,实现安全的线程间数据交换。

LinkedBlockingQueue简介

LinkedBlockingQueue是Java中的一个阻塞队列实现,它基于链表数据结构实现,可以用于在生产者和消费者之间安全地传递数据。

该队列的特点如下:

  • 队列无界(可以设置最大容量),或者有界(如果没有指定容量,则默认为Integer.MAX_VALUE)。
  • 当队列为空时,消费者线程将被阻塞,直到有新的元素被添加到队列中。
  • 当队列已满时,生产者线程将被阻塞,直到队列有空闲位置。
  • 支持公平性策略,可以通过构造函数参数来指定是否采用公平性策略。在公平模式下,线程按照加入队列的顺序竞争访问资源。

LinkedBlockingQueue是一个基于链表的阻塞队列,其实现原理如下:

  1. 内部数据结构:LinkedBlockingQueue使用一个双向链表来存储元素。每个节点包含一个元素和指向前一个节点和后一个节点的引用。这种链表结构可以高效地支持在队列两端进行插入和删除操作。
  2. 队列大小控制:LinkedBlockingQueue可以有界或无界。在构造函数中可以指定队列的最大容量。如果未指定队列大小,则默认为Integer.MAX_VALUE,即无界队列。
  3. 读写锁:LinkedBlockingQueue使用ReentrantLock可重入锁来实现对队列的并发访问控制。它允许多个读操作同时进行,但只允许一个写操作进行。
  4. 条件变量:LinkedBlockingQueue使用两个Condition条件变量,notEmpty和notFull,来控制生产者和消费者线程的阻塞与唤醒。当队列为空时,消费者线程会在notEmpty条件上等待;当队列已满时,生产者线程会在notFull条件上等待。当有元素被添加到队列中或从队列中移除时,对应的条件变量会通知等待的线程。

通过以上的实现机制,LinkedBlockingQueue能够实现线程安全的插入和删除操作,并且支持阻塞和非阻塞的操作。它提供了put、take等方法来实现元素的添加和移除,并且在队列为空或已满时能够自动阻塞和唤醒相应的线程,以保证生产者和消费者之间的同步和协作。

具体使用LinkedBlockingQueue时,可以通过以下方法进行操作:

  • put(E e): 将指定元素插入队列尾部,如果队列已满则阻塞等待。
  • take(): 移除并返回队列头部的元素,如果队列为空则阻塞等待。
  • offer(E e): 将指定元素插入队列尾部,如果队列已满则返回false,不阻塞。
  • poll(): 移除并返回队列头部的元素,如果队列为空则返回null,不阻塞。

除了上述方法外,LinkedBlockingQueue还提供了其他一些方法,例如size()返回队列的元素数量,isEmpty()判断队列是否为空,clear()清空队列等。

LinkedBlockingQueue提供了线程安全的操作,适用于多线程环境下的生产者和消费者模型。它的阻塞特性使得生产者和消费者线程可以有效地进行同步,以实现数据的安全传递。

LinkedBlockingQueue的主要属性

put方法详解

put方法是LinkedBlockingQueue类提供的一个用于向队列中添加元素的方法。具体详解如下:

  • 参数:put方法接受一个参数,即待添加到队列中的元素。
  • 阻塞操作:如果队列已满,即达到了最大容量,put方法会导致当前线程阻塞,直到有空闲空间可用或线程被中断。
  • 元素添加:当满足添加条件时,put方法会将元素添加到队列的末尾。
  • 并发性:put方法使用ReentrantLock可重入锁实现对队列的并发访问控制,确保在多线程环境下添加操作的线程安全性。
  • 队列状态:添加元素后,队列的大小会增加一个单位。
  • 返回值:put方法没有显式的返回值,它是一个无结果的操作。

take方法详解

take方法是LinkedBlockingQueue类提供的一个用于从队列中取出元素的方法。具体详解如下:

  • 阻塞操作:如果队列为空,即没有任何元素可用,take方法会导致当前线程阻塞,直到有元素可用或线程被中断。
  • 元素移除:当满足移除条件时,take方法会从队列的头部移除一个元素,并返回该元素。
  • 并发性:take方法使用ReentrantLock可重入锁实现对队列的并发访问控制,确保在多线程环境下取出操作的线程安全性。
  • 队列状态:移除元素后,队列的大小会减少一个单位。
  • 返回值:take方法会返回被移除的元素。

需要注意的是,put和take方法都是阻塞操作,它们会导致当前线程等待直到满足添加或移除的条件。这使得LinkedBlockingQueue可以作为一种线程安全的同步机制,用于实现生产者和消费者之间的协作。当队列为空时,take方法会阻塞消费者线程,直到有新的元素可供消费。当队列已满时,put方法会阻塞生产者线程,直到有空闲空间可供添加新的元素。

LinkedBlockingQueue代码示例

import java.util.concurrent.LinkedBlockingQueue;

public class main {
    public static void main(String[] args) {
        LinkedBlockingQueue queue = new LinkedBlockingQueue<>(10);

        // 启动生产者线程
        for (int i = 1; i <= 3; i++) {
            final int producerId = i;
            new Thread(() -> {
                for (int j = 1; j <= 5; j++) {
                    try {
                        int element = producerId * 10 + j;
                        System.out.println("生产者" + producerId + "生产元素:" + element);
                        queue.put(element); // 将元素添加到队列
                        Thread.sleep((long) (Math.random() * 1000)); // 模拟生产过程
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

        // 启动消费者线程
        for (int i = 1; i <= 2; i++) {
            final int consumerId = i;
            new Thread(() -> {
                while (true) {
                    try {
                        Integer element = queue.take(); // 从队列中取出元素
                        System.out.println("消费者" + consumerId + "消费元素:" + element);
                        Thread.sleep((long) (Math.random() * 2000)); // 模拟消费过程
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

在上述代码中,我们创建了一个最大容量为10的LinkedBlockingQueue对象。然后启动了3个生产者线程和2个消费者线程。生产者线程通过循环向队列中添加元素,消费者线程通过循环从队列中取出元素,并打印到控制台上。

为了模拟真实的生产和消费过程,我们在生产者和消费者线程中都添加了随机的睡眠操作。

运行上述代码,可以看到类似以下的输出结果:

生产者1生产元素:11
生产者3生产元素:31
生产者2生产元素:21
消费者1消费元素:11
消费者2消费元素:31
生产者3生产元素:32
生产者3生产元素:33
生产者1生产元素:12
生产者1生产元素:13
生产者2生产元素:22
生产者1生产元素:14
消费者2消费元素:21
消费者2消费元素:32
生产者3生产元素:34
生产者1生产元素:15
生产者2生产元素:23
消费者1消费元素:33
生产者3生产元素:35
生产者2生产元素:24
消费者2消费元素:12
生产者2生产元素:25
消费者1消费元素:13
消费者1消费元素:22
消费者2消费元素:14
消费者1消费元素:34
消费者1消费元素:15
消费者2消费元素:23
消费者2消费元素:35
消费者1消费元素:24
消费者2消费元素:25

在这个例子中,我们使用了多个生产者和消费者线程,它们同时操作LinkedBlockingQueue,展示了队列的并发性质。

ArrayBlockingQueue与LinkedBlockingQueue的比较

相同点:ArrayBlockingQueue和LinkedBlockingQueue都是通过condition通知机制来实现可阻塞式插入和删除元素,并满足线程安全的特性。

不同点

  • 数据结构:ArrayBlockingQueue使用数组作为底层数据结构,而LinkedBlockingQueue使用链表作为底层数据结构。
  • 容量限制:ArrayBlockingQueue的容量是固定的,需要在创建时指定容量大小;而LinkedBlockingQueue的容量可以选择不限制大小(默认Integer.MAX_VALUE)或者在创建时指定容量大小。
  • 性能:由于ArrayBlockingQueue使用数组实现,插入和移除元素时无需进行链表的节点操作,因此在高并发环境下,ArrayBlockingQueue的性能通常比LinkedBlockingQueue更好。
  • 并发度:ArrayBlockingQueue在插入和删除元素时只需要获取一个全局锁,即lock,这意味着多个线程无法同时进行插入和删除操作,可能会限制并发度;而LinkedBlockingQueue在插入和删除元素时分别采用了putLock和takeLock,使得多个线程可以同时进行操作,从而提高了并发度。

总的来说,如果对并发度要求不高,且需要固定大小的有界队列,可以选择ArrayBlockingQueue;如果对并发度要求较高,且需要动态调整大小的无界队列,可以选择LinkedBlockingQueue。

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