目录
BlockingQueue简介
BlockingQueue是Java并发包中用于实现线程间协作和数据交换的容器类。在多线程编程中,由于线程之间的竞争条件和临界区问题,需要使用线程安全的数据结构来保证数据的正确性和一致性。而BlockingQueue提供了一种简单且高效的方式来实现线程间的同步和协作。
阻塞队列的特点是,当队列为空时,消费者线程将被阻塞,直到队列中有新的元素;当队列已满时,生产者线程将被阻塞,直到队列中有空位可以插入新的元素。通过这种机制,可以有效地实现生产者-消费者模式,并使得线程之间的通信更加简单和高效。
阻塞队列提供了一系列的方法,如put、take、offer、poll等。其中,put和take方法是阻塞的,当队列已满或为空时,线程将被阻塞等待;offer和poll方法则提供了超时等待的功能,如果在指定的超时时间内未能插入或获取元素,则返回特定的结果。
Java并发包提供了多个实现BlockingQueue接口的具体类,如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。每种类型的阻塞队列都有其特定的应用场景和性能特点,根据实际需求选择合适的阻塞队列类是非常重要的。
总而言之,BlockingQueue是一种线程安全的容器类,通过提供阻塞等待的操作,可以简化线程间的协作和数据交换。它在多线程编程中广泛应用于“生产者-消费者”模式等场景,帮助我们实现高效且正确的多线程程序。
常用方法
BlockingQueue的常用方法可以分为插入元素、删除元素和查看元素三个方面。
插入元素的操作包括:
- add(E e):在队列尾部插入元素e,如果队列已满则抛出异常。
- offer(E e):在队列尾部插入元素e,如果队列已满则返回false。
- put(E e):在队列尾部插入元素e,如果队列已满则阻塞等待直到有空位可用。
- offer(E e, long timeout, TimeUnit unit):在给定的超时时间内,在队列尾部插入元素e,如果队列仍然已满则返回false。
删除元素的操作包括:
- remove(Object o):从队列中删除指定元素o,如果删除成功则返回true,否则返回false。
- poll():移除并返回队列头部的元素,如果队列为空则返回null。
- take():移除并返回队列头部的元素,如果队列为空则阻塞等待直到有元素可用。
- poll(long timeout, TimeUnit unit):在给定的超时时间内,移除并返回队列头部的元素,如果超时仍然没有可用元素则返回null。
查看元素的操作包括:
- element():返回队列头部的元素,如果队列为空则抛出异常。
- peek():返回队列头部的元素,如果队列为空则返回null。
简而言之,通过这些操作,我们可以方便地向队列中插入和删除元素,并且可以查看队列的头部元素。同时,由于BlockingQueue的特殊性,它还提供了阻塞等待和超时等待的功能,使得在多线程环境中的数据交换更加方便和安全。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class main {
public static void main(String[] args) {
// 创建一个容量为3的阻塞队列
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
// 插入元素到队列
try {
queue.put(1); // 阻塞等待,直到队列有足够的空间
System.out.println("插入元素 1");
boolean offered = queue.offer(2); // 尝试插入元素 2,如果队列已满则返回false
if (offered) {
System.out.println("插入元素 2 成功");
} else {
System.out.println("插入元素 2 失败");
}
queue.add(3); // 尝试插入元素 3,如果队列已满会抛出异常
System.out.println("插入元素 3");
} catch (InterruptedException e) {
e.printStackTrace();
}
// 查看队列头部元素
Integer peekedElement = queue.peek();
if (peekedElement != null) {
System.out.println("队列头部元素为:" + peekedElement);
} else {
System.out.println("队列为空");
}
// 删除队列元素
Integer removedElement = queue.poll(); // 删除队列头部元素
if (removedElement != null) {
System.out.println("删除元素:" + removedElement);
} else {
System.out.println("队列为空,无法删除元素");
}
// 查看队列头部元素
Integer element = queue.element();
System.out.println("队列头部元素为:" + element);
}
}
在这个示例中,我们创建了一个容量为3的阻塞队列,并向队列插入了三个元素。然后我们使用peek()方法查看队列的头部元素,使用poll()方法删除队列的头部元素,最后使用element()方法再次查看队列的头部元素。根据队列的状态和操作结果,会输出相应的信息。
请注意,在使用put()方法和take()方法时,如果队列已满或者队列为空,线程会被阻塞等待直到有空间可用或者有元素可以获取。
常用的BlockingQueue
实现BlockingQueue接口的有ArrayBlockingQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedTransferQueue, PriorityBlockingQueue, SynchronousQueue,而这几种常见的阻塞队列也是在实际编程中会常用的,下面对这几种常见的阻塞队列进行详细说明:
ArrayBlockingQueue:
ArrayBlockingQueue 是一个由数组实现的有界阻塞队列,它按照先进先出(FIFO)的原则对元素进行操作。队列的头部元素是队列中存在时间最长的元素,而尾部元素则是最新加入的元素。当队列已满时,尝试将元素放入队列会导致操作被阻塞;当队列为空时,尝试从队列取出元素同样也会被阻塞。
默认情况下,ArrayBlockingQueue 不保证线程对队列的访问顺序是公平的。公平性指的是按照线程等待的绝对时间顺序访问 ArrayBlockingQueue,即最早等待的线程将最先获得访问权限。没有公平性则意味着访问 ArrayBlockingQueue 的顺序不一定遵循严格的时间顺序,可能存在某些长时间阻塞的线程依然无法获得访问权限的情况。如果需要一个具有公平性的 ArrayBlockingQueue,可以在创建时通过设置 fair 参数为 true 来实现,例如:
private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10, true);
这样设置后,当多个线程竞争访问该队列时,会按照它们等待的绝对时间顺序来决定访问权限,从而实现公平性。需要注意的是,保证公平性通常会降低吞吐量。
DelayQueue:
DelayQueue 是一个存放实现了 Delayed 接口的数据对象的无界阻塞队列。只有当数据对象的延时时间到达时,才能将其插入到队列中进行存储。如果当前所有的数据对象都还没有达到创建时所指定的延时期,则队列没有队头,并且线程通过调用 poll 等方法来获取数据元素时会返回 null。
所谓数据延时期满,是通过调用 Delayed 接口的 getDelay(TimeUnit.NANOSECONDS) 方法进行判定。如果该方法返回的值小于等于 0,则说明该数据元素的延时期已满,可以从队列中取出。
DelayQueue 可以用于实现按照延时时间排序的任务调度器或者定时任务执行器。通过设置合适的延时时间,可以在指定的时间点执行任务或者按照一定的延时顺序处理数据。
LinkedBlockingDeque:
LinkedBlockingDeque 是一种基于链表数据结构的有界阻塞双端队列。当创建 LinkedBlockingDeque 对象时未指定大小时,默认大小为 Integer.MAX_VALUE。与 LinkedBlockingQueue 相比,LinkedBlockingDeque 具有双端队列的特性。
LinkedBlockingDeque 的基本操作可以分为四种类型:
- 特殊情况,抛出异常:当队列已满时,插入操作会抛出异常;当队列为空时,移除操作会抛出异常。
- 特殊情况,返回特殊值:当队列已满时,插入操作返回特殊值(如 null 或 false);当队列为空时,移除操作返回特殊值(如 null)。
- 线程阻塞:当线程不满足操作条件时,如插入操作需要等待队列有空闲位置,移除操作需要等待队列有元素可供移除,线程会被阻塞直至条件满足。
- 操作具有超时特性:队列提供的某些操作可以设置超时时间,在超过指定时间后仍无法满足操作条件,则返回特殊值(如 null 或 false)。
此外,LinkedBlockingDeque 实现了 BlockingDeque 接口,而 LinkedBlockingQueue 实现的是 BlockingQueue 接口。这两个接口的功能在大多数情况下是可以等价使用的。比如,BlockingQueue 的 add 方法和 BlockingDeque 的 addLast 方法功能是相同的。
LinkedBlockingDeque 在实现双端队列的基础上,提供了阻塞特性,使得多线程操作队列更加安全和灵活。
LinkedBlockingQueue:
LinkedBlockingQueue是一个由链表实现的有界阻塞队列,同样满足先入先出(FIFO)的特性。相比于 ArrayBlockingQueue,它具有更高的吞吐量。为了防止 LinkedBlockingQueue 增长过快并消耗大量内存,通常在创建该对象时会指定其大小。如果没有指定大小,容量将等于 Integer.MAX_VALUE。
LinkedBlockingQueue 通常被用作多线程环境下的数据共享容器。当生产者生成数据并放入该队列时,如果队列已满,则会阻塞直到队列有空间;当消费者消费数据并从队列中取出元素时,如果队列为空,则会阻塞直到队列中有新的数据。因此,LinkedBlockingQueue 具有线程安全和阻塞的特性,适用于并发编程场景。
需要注意的是,在使用 LinkedBlockingQueue 时,应该尽量避免使用无限制大小的队列。如果队列容量过大,可能会导致内存占用过高,影响系统的稳定性和可用性。
LinkedTransferQueue:
LinkedTransferQueue 是一种由链表数据结构构成的无界阻塞队列,并且实现了 TransferQueue 接口。相比于其他阻塞队列,LinkedTransferQueue 具有一些不同的方法。
- transfer(E e): 当当前有线程(消费者)正在调用 take() 方法或具有延时特性的 poll() 方法来消费数据时,生产者线程可以调用 transfer 方法将数据传递给消费者线程。如果当前没有消费者线程,则生产者线程将数据插入队尾并等待,直到有消费者线程能够接收数据才会退出。
- tryTransfer(E e): tryTransfer 方法可以立即将数据传输给消费者线程,前提是当前有消费者线程(调用 take 方法或具有超时特性的 poll 方法)正在等待消费数据。如果当前没有消费者线程等待消费数据,则立即返回 false。与 transfer 方法不同,tryTransfer 方法可以立即返回结果退出,而不需要等到有消费者线程接收数据。
- tryTransfer(E e, long timeout, TimeUnit unit): 该方法与 transfer 方法具有相同的基本功能,但增加了超时特性。如果在规定的超时时间内没有消费者线程准备消费数据,则返回 false。
LinkedTransferQueue 提供了一种高级的数据传输机制,适用于一些特殊的应用场景,其中生产者线程和消费者线程之间需要进行数据的直接传输。它能够提供更灵活的控制机制,以满足具体的需求。
PriorityBlockingQueue:
PriorityBlockingQueue 是一个支持优先级的无界阻塞队列。它的特点是元素可以根据优先级进行排序。默认情况下,元素使用自然顺序进行排序,也就是实现了 Comparable 接口的对象会根据其 compareTo() 方法的返回值进行排序。你也可以通过自定义类来实现 compareTo() 方法来定义元素的排序规则。此外,你还可以在初始化 PriorityBlockingQueue 时通过构造器参数 Comparator 来指定元素的排序规则。
当元素被插入到 PriorityBlockingQueue 中时,它们会按照指定的排序规则自动进行排序。当你从队列中取出元素时,将会返回优先级最高的元素(根据排序规则确定)。如果队列为空,获取操作将会被阻塞,直到队列中有可用元素。
需要注意的是,PriorityBlockingQueue 是无界队列,因此理论上可以不断添加元素。但是,在实际应用中,为了避免内存溢出等问题,通常需要限制队列大小或者采用其他方式来控制队列的增长。
SynchronousQueue:
SynchronousQueue 是一种特殊的阻塞队列,它没有实际存储任何数据元素。每个插入操作必须等待另一个线程进行相应的删除操作,反之亦然。因此,SynchronousQueue 可以看作是一个传递数据的通道,用于在两个线程之间进行数据交换。
当一个线程尝试将元素插入到 SynchronousQueue 中时,如果没有其他线程正在等待删除操作,则插入操作将被阻塞,直到有另一个线程等待删除操作为止。同样地,当一个线程尝试从 SynchronousQueue 中删除元素时,如果没有其他线程正在等待插入操作,则删除操作将被阻塞,直到有另一个线程等待插入操作为止。
SynchronousQueue 还可以通过构造器参数来指定公平性。如果指定了公平性,那么等待时间较长的线程将优先于等待时间较短的线程执行插入或删除操作。
需要注意的是,SynchronousQueue 并不存储数据,因此它具有非常小的内存开销。它适用于一些特定的场景,例如在生产者和消费者之间进行高效的数据传输。
这些常见的阻塞队列提供了不同的功能和特性,可以根据具体的需求来选择使用。它们在多线程编程中起到了重要的作用,帮助解决了线程之间的并发访问问题。