BlockingQueue
BlockingQueue
是Java并发包(java.util.concurrent)中提供的一个阻塞队列接口,它继承自 Queue
接口。
BlockingQueue
中的元素采用 FIFO 的原则,支持多线程环境并发访问,提供了阻塞读取和写入的操作,当前线程在队列满或空的情况下会被阻塞,直到被唤醒或超时。
常用的实现类有:
ArrayBlockingQueue
:并发容器 ArrayBlockingQueue 详解LinkedBlockingQueue
:并发容器 LinkedBlockingQueue 详解PriorityBlockingQueue
:并发容器 PriorityBlockingQueue 详解SynchronousQueue
LinkedBlockingDeque
:并发容器 LinkedBlockingDeque 详解
等类,它们的实现方式各有不同。
适用场景
BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。
一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。
如果该阻塞队列到达了其临界点,生产者线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,直到消费者线程从队列中拿走一个对象。
消费者线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。
常用方法
-
put(E e)
:将元素 e 插入到队列中,如果队列已满,则会阻塞当前线程,直到队列有空闲空间 -
offer(E e)
:将元素 e 插入到队列中,如果队列已满,则返回 false。 -
offer(E element, long timeout, TimeUnit unit)
方法是BlockingQueue
:在指定的时间内将元素添加到队列中。-
timeout
:超时时间,表示在指定的时间内等待队列空间可用。如果超过指定的时间仍然无法将元素添加到队列中,将返回 false。 -
unit
:超时时间的单位。
-
-
take()
:移除并返回队列头部的元素,如果队列为空,则会阻塞当前线程,直到队列有元素 -
poll()
:移除并返回队列头部的元素,如果队列为空,则返回null
-
poll(long timeout, TimeUnit unit)
:在指定的时间内从队列中检索并移除元素。返回移除的元素。如果超过指定的时间仍然没有可用的元素,将返回 null。 -
peek()
:返回队列头部的元素,但不会移除。如果队列为空,则返回null
-
size()
:返回队列中元素的数量 -
isEmpty()
:判断队列是否为空,为空返回 true,否则返回 false -
isFull()
:判断队列是否已满,已满返回 true,否则返回 false -
clear()
:清空队列中的所有元素
行为 | 抛异常 | 返回特定值 | 阻塞 | 超时 | |
---|---|---|---|---|---|
插入 | add(o) | offer(o) | put(o) | offer(o, timeout, timeunit) | |
移除 | remove() | poll() | take() | poll(timeout, timeunit) | |
检查 | element() | peek() |
-
抛异常: 如果试图的操作无法立即执行,抛一个异常。
-
特定值: 如果试图的操作无法立即执行,返回 true / false / null)。
-
阻塞: 如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
-
超时: 如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是 true / false)。
若向 BlockingQueue
中插入 null,将会抛出 NullPointerException
死锁问题
需要注意的是,在使用 BlockingQueue
时要注意防止死锁的问题:
-
在队列满之后调用
offer()
方法插入元素会返回false
,此时不能直接调用put()
方法,因为在插入之前还需要获取其它资源,如果在获取资源时一直阻塞在这里,就会发生死锁。 -
为了防止死锁的问题,建议使用
offer(E e, long timeout, TimeUnit unit)
和poll(long timeout, TimeUnit unit)
带有超时时间的方法。
TransferQueue
TransferQueue
是 JUC 中的一个接口,它继承了 BlockingQueue
接口,在 BlockingQueue
的基础上扩展了一个 transfer()
方法,提供了一种更强大的交互方式。
与 BlockingQueue
不同,TransferQueue
提供了 "一对一 "和 "多对一" 两种传输模式:
-
一对一模式:指等待的线程必须与指定线程直接配对,相当于是生产者把元素需要传递给对应的消费者 ,如果没有对应的消费者,则生产者线程会被阻塞。
-
多对一模式:指等待的线程可以与任何线程交互,即生产者线程不需与特定的消费者线程配对,线程池中的任意线程都可以接收到元素。
扩展方法
TransferQueue
接口扩展了以下方法:
-
transfer(E e)
: 将指定的元素发送给一个消费者线程,如果没有等待的消费者线程,则当前线程被阻塞,直到有消费者线程接收该元素。 -
tryTransfer(E e)
: 将指定的元素发送给一个消费者线程,如果没有等待的消费者线程,则立即返回 false,否则返回 true。 -
tryTransfer(E e, long timeout, TimeUnit unit)
: 将指定的元素发送给一个消费者线程,如果没有等待的消费者线程则在指定时间内等待,如果等待超时则返回 false,否则返回 true。 -
hasWaitingConsumer()
: 判断是否有消费者线程在等待接收元素。 -
getWaitingConsumerCount()
: 获取等待接收元素的消费者线程数量。
需要注意的是,由于其阻塞特性,TransferQueue
在并发环境下使用时需要注意线程安全性。
LinkedTransferQueue
LinkedTransferQueue
是一个无界的、基于链表的阻塞队列,于 JDK 7 被引入。
与其他并发队列不同,LinkedTransferQueue
通过 transfer()
方法将元素直接发送给消费者线程,如果没有消费者线程等待接收元素,那么生产者线程将会阻塞(一对一传输)。
内部原理
LinkedTransferQueue
基于链表实现,其内部节点分为数据节点和请求节点。数据节点用于存储实际的数据元素,而请求节点则用于表示消费者的取数请求。
-
当消费者线程尝试从队列中取数据时,如果队列为空,则消费者线程会生成一个请求节点并放入队列中等待。
-
生产者线程在添加数据时,会检查队列头部是否有等待的消费者请求节点,如果有,则直接将元素传递给该消费者并唤醒其线程。
特点
-
无界队列 :
LinkedTransferQueue
是无界的,它允许任意数量的元素加入队列,因此无需担心队列溢出的问题。然而,在实际应用中,由于它内部设计的高效性,通常不会导致内存无限增长。 -
直接传递 :
LinkedTransferQueue
提供了一种机制,使得生产者可以直接将元素传输给等待的消费者,而无需通过中间缓冲区。当调用transfer(E e)
方法时,如果有一个消费者正在等待接收元素,那么元素会立即从生产者转移到消费者,并且两个线程之间的交换无需锁或其他同步机制。 -
高性能低延迟 :
LinkedTransferQueue
通过原子操作和 CAS(Compare-And-Swap)算法保证了高度的并发性能和较低的线程上下文切换开销。此外,它还使用了一些优化技术(如自旋锁)来减少无效通知(如虚假唤醒),从而提高效率。 -
混合支持阻塞和非阻塞操作 :除了基本的插入(offer)、移除(poll)和检查(peek)等操作外,
LinkedTransferQueue
还提供了tryTransfer(E e)
等额外的方法,这些方法支持无等待的元素传输,即尝试立即将元素传输给一个等待的消费者,如果成功则返回true,否则返回false。
应用场景
-
生产者-消费者模型 :
LinkedTransferQueue
最常见的应用场景就是生产者-消费者模型。生产者向队列中添加数据,而消费者则从队列中获取数据。由于LinkedTransferQueue
支持直接传递和高效的并发性能,因此特别适用于需要高吞吐量和低延迟的数据传输场景。 -
多线程数据传输 :多个线程可以同时向
LinkedTransferQueue
中添加数据,同时也可以同时从队列中获取数据。由于LinkedTransferQueue
的线程安全性,因此可以保证数据传输的正确性和可靠性。 -
消息队列 :
LinkedTransferQueue
还可以用于实现消息队列。生产者向队列中添加消息,而消费者则从队列中获取消息。由于LinkedTransferQueue
的高效性,因此可以实现高吞吐量的消息传输。 -
线程池工作队列 :在 Java 线程池中,
LinkedTransferQueue
可以作为工作队列使用。线程池中的线程从队列中获取任务并执行,由于LinkedTransferQueue
的线程安全性和高效性,因此可以保证任务的正确性和可靠性,并提高线程池的整体性能。
构造方法
- 创建一个空的
LinkedTransferQueue
:
java
LinkedTransferQueue()
- 创建一个包含给定集合中所有元素的
LinkedTransferQueue
,元素存储顺序为集合迭代器返回元素的顺序。如果给定集合为空,则创建一个空的LinkedTransferQueue
。
java
LinkedTransferQueue(Collection<? extends E> collection)
模式转换
-
一对一(生产者)模式(Transfer Mode):
-
生产者线程使用
transfer(E e)
方法将元素放入队列中。如果队列中没有等待的消费者线程,则生产者线程会被阻塞,直到有消费者线程从队列中获取该元素。 -
transfer(E e)
方法是一种阻塞方法,它会等待直到元素被消费者线程接收或者队列关闭。
-
-
多对一(消费者)模式(Try Mode):
-
消费者线程使用
tryTransfer(E e)
方法尝试从队列中获取元素。如果队列中没有元素可以立即获取,那么该方法会立即返回false
,而不会阻塞线程。 -
tryTransfer(E e)
方法是一种非阻塞方法,它无需等待,直接返回获取结果。
-
LinkedTransferQueue 使用示例
LinkedTransferQueue
是一种高性能的阻塞队列,它支持在生产者和消费者之间直接传递对象。与普通的阻塞队列相比,LinkedTransferQueue
具有更高的性能,因为它减少了对象的复制次数,并且支持更细粒度的并发控制。
java
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;
public class LinkedTransferQueueExample {
public static void main(String[] args) {
// 创建一个 LinkedTransferQueue 实例
LinkedTransferQueue<String> transferQueue = new LinkedTransferQueue<>();
// 创建生产者线程
Thread producer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println("生产者生产了数据: " + i);
transferQueue.transfer("Data " + i); // 将数据放入队列
TimeUnit.MILLISECONDS.sleep(200); // 模拟生产延迟
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 设置线程中断状态
System.err.println("生产者线程被中断");
}
}
});
// 创建消费者线程
Thread consumer = new Thread(() -> {
while (true) {
try {
String data = transferQueue.take(); // 从队列中取出数据
System.out.println("消费者消费了数据: " + data);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 设置线程中断状态
System.err.println("消费者线程被中断");
break; // 终止循环
}
}
});
// 启动生产者线程和消费者线程
producer.start();
consumer.start();
// 等待生产者线程结束
try {
producer.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 设置线程中断状态
System.err.println("主线程被中断");
}
// 中断消费者线程
consumer.interrupt();
}
}
SynchronousQueue
SynchronousQueue
(同步队列)是 JUC 中的一个没有任何容量的阻塞队列 ,只支持 一对一 模式。
SynchronousQueue
的每个插入操作必须等待另一个线程的对应移除操作 ,反之亦然。也就是说,它是一种没有存储元素的阻塞队列,插入和移除操作是成对出现的。
由于 SynchronousQueue
是一个同步队列,它的操作是阻塞的,即插入和移除操作都必须等待对方的配对操作。这使得 SynchronousQueue
很适用于线程之间的数据交换,尤其是在生产者-消费者模型中的数据传递。
实现原理
SynchronousQueue
的内部实现依赖于两种数据结构:
-
一种是先入先出的队列(TransferQueue),用于公平模式
-
另一种是后入先出的栈(TransferStack),用于非公平模式。
这两种数据结构都继承自一个抽象类 Transferer
,该类定义了一个 transfer
方法,用于执行 put
或 take
操作。在 transfer
方法中,通过自旋和 LockSupport 的 park/unpark
方法来实现线程的阻塞和唤醒,从而实现了线程间的直接通信。
特点
-
不存储元素 :与其他阻塞队列不同,
SynchronousQueue
内部不存储任何元素。它仅仅作为线程间传递对象的媒介。 -
直接传递 :
SynchronousQueue
实现了线程间的直接对象传递,即一个线程通过put操作放入对象后,必须等待另一个线程通过take操作取走对象,然后put操作才会返回。 -
公平性选择 :
SynchronousQueue
提供了两种构造方法,一种是默认的非公平模式,另一种是公平模式。在公平模式下,等待时间较长的线程会优先得到插入或移除元素的机会。 -
高效性 :由于
SynchronousQueue
不存储元素,因此它避免了在内存中存储元素所需的空间和时间开销,使得线程间的通信更加高效。
应用场景
-
生产者-消费者模型 :在生产者-消费者模型中,
SynchronousQueue
可以作为生产者和消费者之间的数据传递通道。生产者将数据放入SynchronousQueue
,消费者从队列中取出数据并处理。 -
线程池任务执行 :在
ThreadPoolExecutor
中,使用SynchronousQueue
作为工作队列可以让提交的任务立即被工作者线程接管处理,避免了任务在队列中的堆积,从而提高了系统的响应速度。 -
事件驱动架构:在事件驱动的系统中,事件可以被立即传递给事件处理器,无需中间缓存,保证了低延迟和高效处理。
-
数据管道 :在需要将数据从一个线程直接传递到另一个线程的场景中,如在并行处理流水线中,
SynchronousQueue
可以作为连接不同处理阶段的桥梁。
构造方法
- 创建一个不带任何元素的
SynchronousQueue
对象,默认使用非公平策略。
java
SynchronousQueue()
-
创建一个带有公平性参数的
SynchronousQueue
对象。如果
fairness
参数为true
,则通常使用公平的排序策略,即等待时间最长的线程被优先处理。如果
fairness
参数为false
,则使用非公平的排序策略,不考虑线程等待时间的先后顺序,而是尽可能地让新到达的线程获取共享资源。
java
SynchronousQueue(boolean fairness)
需要注意的是,SynchronousQueue
的构造方法不支持指定容量或初始元素集合。因为 SynchronousQueue
中每个插入操作必须等待另一个线程对应的删除操作,反之亦然。所以其大小不可预知。
SynchronousQueue 使用示例
SynchronousQueue
是一个特殊的阻塞队列,它实际上不存储任何元素。它的特点是,当一个线程尝试将元素插入队列时,必须有另一个线程准备从队列中取走这个元素,否则插入操作会一直阻塞。同样地,当一个线程尝试从队列中取走元素时,必须有一个线程准备插入元素,否则取走操作也会阻塞。
java
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
public class SynchronousQueueExample {
public static void main(String[] args) {
// 创建一个 SynchronousQueue 实例
SynchronousQueue<String> syncQueue = new SynchronousQueue<>();
// 创建生产者线程
Thread producer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println("生产者生产了数据: " + i);
syncQueue.put("Data " + i); // 将数据放入队列
TimeUnit.MILLISECONDS.sleep(200); // 模拟生产延迟
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 设置线程中断状态
System.err.println("生产者线程被中断");
}
}
});
// 创建消费者线程
Thread consumer = new Thread(() -> {
while (true) {
try {
String data = syncQueue.take(); // 从队列中取出数据
System.out.println("消费者消费了数据: " + data);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 设置线程中断状态
System.err.println("消费者线程被中断");
break; // 终止循环
}
}
});
// 启动生产者线程和消费者线程
producer.start();
consumer.start();
// 等待生产者线程结束
try {
producer.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 设置线程中断状态
System.err.println("主线程被中断");
}
// 中断消费者线程
consumer.interrupt();
}
}