什么是队列
队列(Queue):与栈相对的一种数据结构,集合(Collection)的一个子类。队列允许在一端进行插入操作,而且在另一端进行删除操作的线性表,栈的特点先进后出,而队列的特点是先进先出。队列的用处很大,比如实现消息队列。
Queue类关系图,如下:
注意:上图是精简版的Queue类关系图,内容基于jdk1.8
Queue队列
1、Queue分类
- **双端队列:**双端队列(Deque)是Queue的子类,也是Queue的补充类,头部和尾部都支持元素插入和删除。
- **阻塞队列:**阻塞队列指的是在元素操作时(添加或者删除),如果没有成功,会阻塞等待执行;例如:当添加元素时,如果队列元素已满,队列会阻塞等待直到有空位再插入。
- **非阻塞队列:**非阻塞队列和阻塞队列相反,会直接返回操作的结果,而非阻塞等待。双端队列也属于非阻塞队列。
2、Queue方法说明
public class QueueDemo1 {
public static void main(String[] args) {
Queue<String> linkedList = new LinkedList<>();
linkedList.add("Dog");
linkedList.add("Camel");
linkedList.add("Cat");
String head = linkedList.peek();
System.out.println("peek头部元素:" + head);
while (!linkedList.isEmpty()) {
System.out.println(linkedList.poll());
}
System.out.println("poll头部元素:" + linkedList.poll());
System.out.println("isEmpty:::::>"+linkedList.isEmpty());
System.out.println("删除元素::::::");
linkedList.remove();
}
}
- add(E):添加元素到队列尾部,成功返回 true,队列超出时抛出异常;
- offer(E):添加元素到队列尾部,成功返回 true,队列超出时返回 false;
- remove():检索并删除此队列的头,若队列为空,它将抛出异常;
- poll():获取并移除队列头部的元素,若队列为空,则返回 null;
- peek():获取但不移除此队列头部的元素,若队列为空,则返回 null;
- element():获取但不移除此队列头部的元素,若队列为空,则抛异常。
BlockingQueue 阻塞队列
1 简介
BlockingQueue是Java中的一个接口,它用于在多线程环境下实现线程安全的数据传输。
它是一个队列,其特点是当队列为空时,从队列中取元素的操作会被阻塞,直到队列中有新元素加入;当队列元素已满时,往队列添加元素也会被阻塞,直到队列有空的位置。
在多线程编程中,BlockingQueue起到了很重要的作用。它可以在多个线程之间进行数据的传递和交换,并且提供了一种简单而高效的同步机制。通过使用BlockingQueue,我们可以避免自己实现锁和条件变量,从而减少了编程的复杂性和错误的概率。
BlockingQueue在Java的并发编程工具包(java.util.concurrent)中被广泛使用,常见的实现类有ArrayBlockingQueue、LinkedBlockingQueue和PriorityBlockingQueue等。它们分别基于数组、链表和优先级队列实现,可以根据实际需求选择适合的实现类使用。
2、功能和用途
BlockingQueue的主要功能和用途如下:
- 线程完全的数据传输:BlockingQueue提供了线程安全的数据传输机制,可以在多个线程之间传递数据。当队列为空时,消费者线程可以阻塞等待,直到生产者线程将数据放入队列;当队列已满时,生产者线程可以阻塞等待,直到消费者把队列数据取出。
- 缓存:BlockingQueue可以作为一个缓存区,当生产者产生数据速度过快,而消费者处理数据比较慢时,可以使用BlockingQueue来平衡生产者和消费者之间的速度差异。生产者可以将数据放入队列,而消费者可以从队列取出数据进行处理,以实现数据的平均处理速度。
- 任务调度:BlockingQueue可以用于线程池中的任务调度。线程池中的线程可以从BlockingQueue中获取任务进行处理,当队列为空时,线程可以阻塞等待,直到队列中有新的任务加入。
- 同步机制:BlockingQueue提供了一个有效的同步机制,可以实现线程之间的协作和同步。通过向BlockingQueue中放入特殊元素,可以实现线程之间的同步和通信,例如使用特殊标志来指示任务的开始和结束。
BlockingQueue实现类
3、API和示例
3.1 常用API
添加元素:
- add(E e): 将指定元素添加到队列尾部,如果队列已满则会抛出异常。
- offer(E e): 将指定元素添加到队列尾部,如果队列已满则返回false。
- put(E e): 将指定元素添加到队列尾部,如果队列已满则阻塞等待直到有空位。
移除元素:
- remove(): 移除并返回队列头部的元素,如果队列为空则抛出异常。
- poll(): 移除并返回队列头部的元素,如果队列为空则返回null。
- take(): 移除并返回队列头部的元素,如果队列为空则阻塞等待直到有元素可取。
获取元素:
- element(): 返回队列头部的元素,如果队列为空则抛出异常。
- peek(): 返回队列头部的元素,如果队列为空则返回null。
其他方法:
- size(): 返回队列中的元素个数。
- isEmpty(): 判断队列是否为空。
- remainingCapacity(): 返回队列中剩余的容量。
- contains(Object o): 判断队列是否包含指定的元素。
- remove(Object o): 移除队列中的指定元素。
以上仅为一些常见的API,具体使用时可以根据需要选择适合的方法。同时,BlockingQueue接口的具体实现类可能会额外提供一些特定的方法,例如阻塞超时等待的方法。
3.2 代码示例
public class BlockingQueueExample {
public static void main(String[] args) {
// 创建一个容量为3的ArrayBlockingQueue
BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(3);
// 生产者线程
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
// 往队列中放入元素
queue.put(i);
System.out.println("生产者放入元素: " + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 消费者线程
Thread consumer = new Thread(() -> {
try {
Thread.sleep(2000); // 等待2秒,让生产者线程先行
while (!queue.isEmpty()) {
// 从队列中取出元素并打印
int element = queue.take();
System.out.println("消费者取出元素: " + element);
Thread.sleep(1000); // 模拟消费者的处理时间
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 启动生产者和消费者线程
producer.start();
consumer.start();
}
}
这个示例中,我们使用ArrayBlockingQueue作为BlockingQueue的实现类,创建了一个容量为3的队列。生产者线程依次向队列中放入5个元素,消费者线程每隔1秒从队列中取出一个元素进行处理。可以看到,当队列满时,生产者线程会被阻塞等待;当队列空时,消费者线程会被阻塞等待,从而实现了线程之间的同步。
4、特性
4.1 阻塞
阻塞队列区别于其他类型的队列的最主要的特点就是“阻塞”这两个字,所以下面重点介绍阻塞功能:阻塞功能使得生产者和消费者两端的能力得以平衡,当有任何一端速度过快时,阻塞队列便会把过快的速度给降下来。实现阻塞最重要的两个方法是 take 方法和 put 方法。
take 方法
take 方法的功能是获取并移除队列的头结点,通常在队列里有数据的时候是可以正常移除的。可是一旦执行 take 方法的时候,队列里无数据,则阻塞,直到队列里有数据。一旦队列里有数据了,就会立刻解除阻塞状态,并且取到数据。过程如图所示:
put 方法
put 方法插入元素时,如果队列没有满,那就和普通的插入一样是正常的插入,但是如果队列已满,那么就无法继续插入,则阻塞,直到队列里有了空闲空间。如果后续队列有了空闲空间,比如消费者消费了一个元素,那么此时队列就会解除阻塞状态,并把需要添加的数据添加到队列中。过程如图所示:
4.2 是否有界
阻塞队列还有一个非常重要的属性,那就是容量的大小,分为有界和无界两种。无界队列意味着里面可以容纳非常多的元素,例如 LinkedBlockingQueue 的上限是 Integer.MAX_VALUE,是非常大的一个数,可以近似认为是无限容量,因为我们几乎无法把这个容量装满。但是有的阻塞队列是有界的,例如 ArrayBlockingQueue 如果容量满了,也不会扩容,所以一旦满了就无法再往里放数据了。
5、优点和适用场景
5.1 优点
- 线程安全性:BlockingQueue是线程安全的,可以在多线程环境下安全地进行数据传输和同步。
- 简化编程:使用BlockingQueue可以简化并发编程的复杂性,无需自行实现线程间的同步和通信机制。
- 高效性:BlockingQueue提供了高效的数据传输方式,可以平衡生产者和消费者之间的速度差异,提高系统的吞吐量和响应性能。
5.2 适用场景
- 生产者-消费者模型:BlockingQueue最常见的应用场景就是用于解决生产者和消费者之间的速度不匹配问题。生产者将数据放入队列,消费者从队列中获取数据进行处理,从而实现了生产者和消费者之间的解耦与平衡。
- 线程池调度:BlockingQueue可以作为线程池中任务队列的实现,用于调度线程池中的任务。线程池中的线程可以从BlockingQueue中获取任务进行处理,当队列为空时,线程可以阻塞等待,直到队列中有任务加入。
- 同步与通信机制:通过BlockingQueue提供的阻塞等待和通知机制,可以实现线程之间的同步和协作。例如可以使用特殊的标识符或特定元素来实现线程间的通信和同步。
总的来说,BlockingQueue适用于多线程环境下的数据传输和同步场景,特别是生产者-消费者模型和线程池调度。它具有简化编程、提供线程安全性和高效性的优点,能够有效地提高系统性能和并发编程的可靠性。
6、工作原理
BlockingQueue的工作原理是基于阻塞和唤醒机制,主要包括以下几个步骤:
- 队列状态判断:当生产者线程尝试向队列添加元素时,会首先判断队列的状态。如果队列已满,生产者线程将被阻塞,直到队列有空闲位置。
- 阻塞等待:如果队列有空闲位置,生产者线程将将元素放入队列,否则将进入阻塞等待状态。阻塞等待是通过调用Lock或synchronized等底层同步机制实现的。
- 唤醒通知:当消费者线程从队列中取出元素时,会首先判断队列的状态。如果队列为空,消费者线程将被阻塞,直到队列有新的元素。
- 队列操作:当队列状态满足条件后,生产者线程或消费者线程将进行相应的操作,如放入元素或取出元素。
- 唤醒阻塞线程:当生产者线程成功向队列添加元素后,会唤醒可能阻塞的消费者线程。同样地,当消费者线程成功从队列中取出元素后,会唤醒可能阻塞的生产者线程。
通过这种阻塞和唤醒的机制,BlockingQueue实现了生产者和消费者线程之间的同步和协作。当队列满时,生产者线程被阻塞等待,直到队列有空闲位置;当队列为空时,消费者线程被阻塞等待,直到队列有新的元素。这样可以有效地平衡生产者和消费者之间的速度差异,保证数据的正确传输和处理。
需要注意的是,BlockingQueue并不会直接使用锁来实现阻塞和唤醒机制,而是使用底层的同步机制来实现,如ReentrantLock、Condition等。不同的BlockingQueue实现类可能会采用不同的底层机制来实现阻塞和唤醒。例如ArrayBlockingQueue使用了ReentrantLock和Condition来实现,而LinkedBlockingQueue则使用了synchronized和wait/notify机制来实现。
7、注意事项和限制
在使用BlockingQueue时,需要注意以下几点事项和限制:
- 容量限制:BlockingQueue的容量是有限的,需要在创建队列时指定容量。如果队列已满,生产者线程尝试往队列中放入元素时将被阻塞等待,直到队列有空闲位置。如果队列为空,消费者线程尝试从队列中取出元素时将被阻塞等待,直到队列有新的元素。
- 阻塞等待时间:BlockingQueue提供了一些方法可以设置阻塞等待的时间。例如,可以使用**offer(E e, long timeout, TimeUnit unit)**方法来设置生产者在队列已满时的等待时间,**poll(long timeout, TimeUnit unit)**方法来设置消费者在队列为空时的等待时间。在超过指定时间后,如果仍然满足条件,线程将被唤醒。
- 公平性:某些BlockingQueue实现类(如ArrayBlockingQueue)支持公平性策略。公平性策略指的是线程按照先进先出的顺序获取锁或资源,避免线程饥饿。公平性策略可能会对性能产生一定的影响,需要根据具体情况进行选择。
- 特殊返回值:BlockingQueue的插入和删除方法具有特殊的返回值。例如,**put(E e)方法在队列已满时将阻塞,而add(E e)**方法在队列已满时将抛出异常。类似地,**take()方法在队列为空时将阻塞,而remove()**方法在队列为空时将抛出异常。应根据具体需求选择适当的方法。
- 对线程中断的响应:BlockingQueue的阻塞操作是可以被中断的,即当线程处于阻塞状态时,调用线程的interrupt()方法会中断操作并抛出InterruptedException异常。在使用阻塞队列时,应注意处理中断异常,避免线程无法正常终止。需要根据具体的需求和场景选择合适的BlockingQueue实现类,并结合上述注意事项和限制来使用。
8、实际应用
BlockingQueue在实际应用中有很广泛的应用场景,以下是几个常见的使用示例:
- 生产者-消费者模型:多线程环境下,生产者线程负责向队列中生产数据,消费者线程负责从队列中消费数据。BlockingQueue可以作为生产者和消费者之间的缓冲区,实现线程间的数据传输和同步。
- 线程池调度:BlockingQueue可以作为线程池中的任务队列的实现,用于存储待执行的任务。线程池中的线程可以从BlockingQueue中获取任务进行处理,当队列为空时,线程可以阻塞等待,直到队列中有任务加入。
- 网络编程:在并发的网络编程中,可以使用BlockingQueue存储和管理请求和响应消息。当请求消息到达时,可以将其放入BlockingQueue中,由特定的线程池来消费并处理。类似地,可以将响应消息放入BlockingQueue,供其他线程来获取和处理。
- 数据流水线处理:在复杂的数据流水线处理中,不同的处理阶段可以使用BlockingQueue来传递数据。每个处理阶段有一个或多个线程,它们从上一个阶段的BlockingQueue中获取数据进行处理,并将处理结果放入下一个阶段的BlockingQueue中。
- 事件驱动编程:在事件驱动编程中,可以使用BlockingQueue来处理和传递事件消息。当事件触发时,可以将事件消息放入BlockingQueue中,由后台线程来处理。
总的来说,BlockingQueue是一种非常实用的并发编程工具,广泛应用于多线程环境下的数据传输和同步场景。它能够平衡生产者和消费者之间的速度差异,提高系统的吞吐量和响应性能。
参考来源:https://zhuanlan.zhihu.com/p/653717342