Java并发基础:LinkedBlockingDeque全面解析!
内容概要
LinkedBlockingDeque
提供了线程安全的双端队列实现,它支持在队列两端高效地进行插入和移除操作,同时具备阻塞功能,能够很好地协调生产者与消费者之间的速度差异,其内部基于链表结构,使得并发性能优异,是处理多线程间数据传递的理想选择。
核心概念
LinkedBlockingDeque
实现了一个线程安全的双端队列(Deque,即 double-ended queue),这个队列在两端都可以添加和移除元素,而且它是阻塞的,意味着当队列为空时,如果线程尝试从队列中取元素,线程会被阻塞,直到队列中有元素可供取出;同样地,如果队列已满,尝试添加元素的线程也会被阻塞,直到队列中有空间可供添加新元素。
举一个生活中的实际案例,比如一个面包店,面包师傅负责生产面包(生产者),顾客来店里买面包(消费者),面包师傅做好面包后,会把它们放在一个展示架上供顾客挑选;顾客则从这个展示架上取走他们想要的面包,这里使用LinkedBlockingDeque
来模拟这个场景。面包师傅(生产者线程)在队列的一端放入新做好的面包(添加元素到队列),而顾客(消费者线程)从队列的另一端取走面包(从队列中移除元素):
- 阻塞特性:如果展示架上没有面包(队列为空),顾客就会被阻塞,直到面包师傅做好新的面包并放到展示架上,同样,如果展示架满了(队列已满),面包师傅就会被阻塞,直到有顾客取走一些面包,腾出空间来放新的面包。
- 双端操作:在这个场景中,虽然通常面包师傅只在一端放面包,顾客在另一端取面包,但双端队列的灵活性意味着也可以轻松改变这个行为,比如,如果有特殊情况,面包师傅可以从展示架上取回一些面包(从队列的另一端移除元素),或者顾客可以预先把他们的面包订单放到展示架上(在队列的另一端添加元素)。
LinkedBlockingDeque
是一个线程安全的双端队列,允许从队列的两端添加和移除元素,并且它是阻塞的,他通常用来解决以下问题:
- 线程安全:在多线程环境中,当多个线程需要访问和修改共享数据时,
LinkedBlockingDeque
提供了一种线程安全的方式来存储和检索这些数据,它内部的同步机制确保了数据的一致性和完整性。 - 阻塞操作:当队列为空时,消费者线程调用
take()
方法会被阻塞,直到生产者线程向队列中添加元素,同样,当队列已满时,生产者线程调用put()
方法也会被阻塞,直到消费者线程从队列中移除元素,这种阻塞行为有助于防止线程在不必要的情况下空转或浪费CPU资源。 - 容量限制:
LinkedBlockingDeque
可以在创建时指定一个最大容量,这个容量限制了队列中可以存储的元素数量,有助于防止内存溢出,当队列达到最大容量时,生产者线程会被阻塞,直到队列中有空间可用。 - 双端操作:与普通的
BlockingQueue
接口实现相比,LinkedBlockingDeque
提供了双端队列的功能,允许从队列的两端添加和移除元素,这为某些特定的应用场景提供了更大的灵活性。 - 高效的并发性能:由于其内部使用链表数据结构,
LinkedBlockingDeque
在处理大量并发操作时通常具有较好的性能,它适用于需要高吞吐量和低延迟的生产者-消费者场景。
代码案例
下面是一个简单的例子,演示了如何使用 LinkedBlockingDeque
类,如下代码:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
public class LinkedBlockingDequeExample {
public static void main(String[] args) throws InterruptedException {
// 创建一个容量为 5 的 LinkedBlockingDeque
BlockingQueue<String> deque = new LinkedBlockingDeque<>(5);
// 启动一个生产者线程,向队列中添加元素
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
String item = "Item-" + i;
deque.put(item); // 当队列满时,该方法会阻塞
System.out.println("Produced: " + item);
Thread.sleep(200); // 模拟生产延迟
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 启动一个消费者线程,从队列中移除元素
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
String item = deque.take(); // 当队列空时,该方法会阻塞
System.out.println("Consumed: " + item);
Thread.sleep(300); // 模拟消费延迟
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 启动生产者和消费者线程
producer.start();
consumer.start();
// 等待两个线程执行完成
producer.join();
consumer.join();
System.out.println("Producer and Consumer threads have finished.");
}
}
在上面代码中,创建了一个 LinkedBlockingDeque
实例,并指定了它的最大容量为 5,然后创建了一个生产者线程和一个消费者线程,生产者线程将循环 10 次,每次生产一个字符串并将其放入队列中,如果队列已满,put
方法将会阻塞直到队列中有空间可用,消费者线程也将循环 10 次,每次从队列中取出一个元素并打印它,如果队列为空,take
方法将会阻塞直到队列中有元素可取。
由于生产者和消费者线程的速度可能不同,LinkedBlockingDeque
作为一个阻塞队列,能够协调这两个线程之间的速度差异,确保它们可以协同工作,而不会因为队列为空或满而导致任何一方停滞不前。
核心API
LinkedBlockingDeque
实现了 BlockingDeque
接口,是一个线程安全的双端队列,以下是 LinkedBlockingDeque
类中一些主要方法的含义:
- add(E e)
将指定的元素插入到此双端队列表示的队列中(即在此双端队列的尾部),如果立即可行且不会违反容量限制,则成功时返回true
,如果当前没有可用的空间,则抛出IllegalStateException
,这是Queue
接口的方法。 - offer(E e)
将指定的元素插入到此双端队列表示的队列中(即在此双端队列的尾部),如果立即可行且不会违反容量限制,则成功时返回true
,如果当前没有可用的空间,则返回false
,这是Queue
接口的方法。 - put(E e) throws InterruptedException
将指定的元素插入到此双端队列表示的队列中(即在此双端队列的尾部),必要时将等待空间变得可用,这是BlockingQueue
接口的方法。 - offer(E e, long timeout, TimeUnit unit)
将指定的元素插入到此双端队列表示的队列中,必要时将等待指定的时间以使空间变得可用,这是BlockingQueue
接口的方法。 - remove()
获取并移除此双端队列表示的队列的头部,如果此双端队列为空,则抛出NoSuchElementException
,这是Queue
接口的方法。 - poll()
获取并移除此双端队列表示的队列的头部,如果此双端队列为空,则返回null
,这是Queue
接口的方法。 - take() throws InterruptedException
获取并移除此双端队列表示的队列的头部,在元素变得可用之前一直等待,这是BlockingQueue
接口的方法。 - poll(long timeout, TimeUnit unit)
获取并移除此双端队列表示的队列的头部,在指定的时间内等待元素变得可用,这是BlockingQueue
接口的方法。 - peek()
获取但不移除此双端队列表示的队列的头部,如果此双端队列为空,则返回null
,这是Queue
接口的方法。 - element()
获取但不移除此双端队列表示的队列的头部,这是Queue
接口的方法。 - push(E e)
将元素推入此双端队列表示的堆栈中(即在此双端队列的头部),如果立即可行且不会违反容量限制,则成功时返回true
,如果当前没有可用的空间,则抛出IllegalStateException
。 - pop()
从此双端队列表示的堆栈中弹出一个元素,如果此双端队列为空,则抛出NoSuchElementException
。 - addFirst(E e), addLast(E e)
将指定的元素插入此双端队列的开头或结尾。 - offerFirst(E e), offerLast(E e)
将指定的元素插入此双端队列的开头或结尾,如果立即可行且不会违反容量限制,则成功时返回true
,如果当前没有可用的空间,则返回false
。 - removeFirst(), removeLast()
获取并移除此双端队列的第一个元素或最后一个元素。 - pollFirst(), pollLast()
获取并移除此双端队列的第一个元素或最后一个元素,如果此双端队列为空,则返回null
。 - getFirst(), getLast()
获取但不移除此双端队列的第一个元素或最后一个元素。 - peekFirst(), peekLast()
获取但不移除此双端队列的第一个元素或最后一个元素,如果此双端队列为空,则返回null
。
核心总结
LinkedBlockingDeque
类它融合了阻塞队列和双端队列的特性,其优点在于高效的并发性能和灵活的两端操作,适合在生产者-消费者场景中使用,能够很好地处理多线程间的数据共享和传递,缺点在高并发下如果队列大小设置不当,可能会导致过多的线程阻塞,影响系统整体性能,此外,由于是基于链表的实现,其内存占用可能相对较高。
END!
往期回顾
Java并发基础:LinkedBlockingDeque全面解析!
Java并发基础:LinkedTransferQueue全面解析!
Java并发基础:LinkedBlockingQueue全面解析!
Java并发基础:Deque接口和Queue接口的区别?
Spring核心基础:全面总结Spring中提供的那些基础工具类!