阻塞队列之LinkedBlockingQueue

本文详细介绍了LinkedBlockingQueue,它内部由单链表实现,读写分离,采用可重入锁保证线程安全。阐述了其构造器、常用操作及原理,还与LinkedBlockingDeque进行了比较。同时提到AtomicInteger相关方法在多线程下的问题。

概述

LinkedBlockingQueue内部由单链表实现,只能从head取元素,从tail添加元素。添加元素和获取元素都有独立的锁,也就是说LinkedBlockingQueue是读写分离的,读写操作可以并行执行。LinkedBlockingQueue采用可重入锁(ReentrantLock)来保证在并发情况下的线程安全。

构造器

LinkedBlockingQueue一共有三个构造器,分别是无参构造器、可以指定容量的构造器、可以穿入一个容器的构造器。如果在创建实例的时候调用的是无参构造器,LinkedBlockingQueue的默认容量是Integer.MAX_VALUE,这样做很可能会导致队列还没有满,但是内存却已经满了的情况(内存溢出)。

1 public LinkedBlockingQueue();   //设置容量为Integer.MAX
2 
3 public LinkedBlockingQueue(int capacity);  //设置指定容量
4 
5 public LinkedBlockingQueue(Collection<? extends E> c);  //穿入一个容器,如果调用该构造器,容量默认也是Integer.MAX_VALUE

LinkedBlockingQueue常用操作

取数据

take():首选。当队列为空时阻塞

poll():弹出队顶元素,队列为空时,返回空

peek():和poll烈性,返回队队顶元素,但顶元素不弹出。队列为空时返回null

remove(Object o):移除某个元素,队列为空时抛出异常。成功移除返回true

 

添加数据

put():首选。队满是阻塞

offer():队满时返回false

 

判断队列是否为空

size()方法会遍历整个队列,时间复杂度为O(n),所以最好选用isEmtpy

 

put元素原理

基本过程:

1.判断元素是否为null,为null抛出异常

2.加锁(可中断锁)

3.判断队列长度是否到达容量,如果到达一直等待

4.如果没有队满,enqueue()在队尾加入元素

5.队列长度加1,此时如果队列还没有满,调用signal唤醒其他堵塞队列

 1  if (e == null) throw new NullPointerException();
 2        
 3         int c = -1;
 4         Node<E> node = new Node<E>(e);
 5         final ReentrantLock putLock = this.putLock;
 6         final AtomicInteger count = this.count;
 7         putLock.lockInterruptibly();
 8         try {
 9             while (count.get() == capacity) {
10                 notFull.await();
11             }
12             enqueue(node);
13             c = count.getAndIncrement();
14             if (c + 1 < capacity)
15                 notFull.signal();
16         } finally {
17             putLock.unlock();
18         }

 

take元素原理

 基本过程:

1.加锁(依旧是ReentrantLock),注意这里的锁和写入是不同的两把锁

2.判断队列是否为空,如果为空就一直等待

3.通过dequeue方法取得数据

3.取走元素后队列是否为空,如果不为空唤醒其他等待中的队列

 1 public E take() throws InterruptedException {
 2         E x;
 3         int c = -1;
 4         final AtomicInteger count = this.count;
 5         final ReentrantLock takeLock = this.takeLock;
 6         takeLock.lockInterruptibly();
 7         try {
 8             while (count.get() == 0) {
 9                 notEmpty.await();
10             }
11             x = dequeue();
12             c = count.getAndDecrement();
13             if (c > 1)
14                 notEmpty.signal();
15         } finally {
16             takeLock.unlock();
17         }
18         if (c == capacity)
19             signalNotFull();
20         return x;
21     }

enqueue()和dequeue()方法实现都比较简单,无非就是将元素添加到队尾,从队顶取走元素,感兴趣的朋友可以自己去看一下,这里就不粘贴了。

 

LinkedBlockingQueue与LinkedBlockingDeque比较

 

LinkedBlockingDeque和LinkedBlockingQueue的相同点在于: 
1. 基于链表 
2. 容量可选,不设置的话,就是Int的最大值

和LinkedBlockingQueue的不同点在于: 
1. 双端链表和单链表 
2. 不存在哨兵节点 
3. 一把锁+两个条件

实例:

 小记:AtomicInteger的getAndIncrment和getAndDcrement()等方法,这些方法分为两步,get和increment(decrement),在get和increment中间可能有其他线程进入,导致多个线程get到的数值是相同的,也会导致多个线程累加后的值其实累加1.在这种情况下,使用volatile也是没有效果的,因为get之后没有对值进行修改,不能触发volatile的效果。

 1 public class ProducerAndConsumer {
 2     public static void main(String[] args){
 3 
 4         try{
 5             BlockingQueue queue = new LinkedBlockingQueue(5);
 6 
 7             ExecutorService executor = Executors.newFixedThreadPool(5);
 8             Produer producer = new Produer(queue);
 9             for(int i=0;i<3;i++){
10                 executor.execute(producer);
11             }
12             executor.execute(new Consumer(queue));
13 
14             executor.shutdown();
15         }catch (Exception e){
16             e.printStackTrace();
17         }
18 
19     }
20 }
21 
22 class Produer implements  Runnable{
23 
24     private BlockingQueue queue;
25     private int nums = 20;  //循环次数
26 
27     //标记数据编号
28     private static volatile AtomicInteger count = new AtomicInteger();
29     private boolean isRunning = true;
30     public Produer(){}
31 
32     public Produer(BlockingQueue queue){
33         this.queue = queue;
34     }
35 
36     public void run() {
37         String data = null;
38         try{
39             System.out.println("开始生产数据");
40             System.out.println("-----------------------");
41 
42           while(nums>0){
43                 nums--;
44                 count.decrementAndGet();
45 
46                 Thread.sleep(500);
47                 System.out.println(Thread.currentThread().getId()+ " :生产者生产了一个数据");
48                 queue.put(count.getAndIncrement());
49             }
50         }catch(Exception e){
51             e.printStackTrace();
52             Thread.currentThread().interrupt();
53         }finally{
54             System.out.println("生产者线程退出!");
55         }
56     }
57 }
58 
59 class Consumer implements Runnable{
60 
61     private BlockingQueue queue;
62     private int nums = 20;
63     private boolean isRunning = true;
64 
65     public Consumer(){}
66 
67     public Consumer(BlockingQueue queue){
68         this.queue = queue;
69     }
70 
71     public void run() {
72 
73         System.out.println("消费者开始消费");
74         System.out.println("-------------------------");
75 
76         while(nums>0){
77             nums--;
78             try{
79                 while(isRunning){
80                     int data = (Integer)queue.take();
81                     Thread.sleep(500);
82                     System.out.println("消费者消费的数据是" + data);
83             }
84 
85             }catch(Exception e){
86                 e.printStackTrace();
87                 Thread.currentThread().interrupt();
88             }finally {
89                 System.out.println("消费者线程退出!");
90             }
91 
92         }
93     }
94 }

效果:

 1 12 :生产者生产了一个数据
 2 11 :生产者生产了一个数据
 3 13 :生产者生产了一个数据
 4 12 :生产者生产了一个数据
 5 消费者消费的数据是-3
 6 11 :生产者生产了一个数据
 7 13 :生产者生产了一个数据
 8 12 :生产者生产了一个数据
 9 消费者消费的数据是-3
10 13 :生产者生产了一个数据
11 11 :生产者生产了一个数据
12 12 :生产者生产了一个数据
13 消费者消费的数据是-3
14 13 :生产者生产了一个数据
15 11 :生产者生产了一个数据
16 消费者消费的数据是-3
17 消费者消费的数据是-3

可以看到,有多个producer在生产数据的时候get到的是相同的值。

转载于:https://www.cnblogs.com/duodushuduokanbao/p/9556555.html

<think>好的,我现在要回答用户的问题,主要是关于LinkedBlockingQueue是否属于阻塞队列,以及线程池为何通常使用阻塞队列而不是非阻塞队列。我需要参考用户提供的引用内容,并结合自己的知识来组织答案。 首先,用户的问题分两部分:确认LinkedBlockingQueue是否为阻塞队列,以及线程池使用阻塞队列的原因。根据引用内容,引用[1]提到LinkedBlockingQueue阻塞队列,引用[4]也讨论了LinkedBlockingQueue作为默认线程池的阻塞队列。所以第一部分应该直接回答LinkedBlockingQueue属于阻塞队列,并解释其特性,比如基于链表、可选有界等。 接下来是线程池使用阻塞队列的原因。用户之前的回答已经详细列过阻塞队列的优势,比如资源管控、线程协作、适应任务波动等。这里需要结合用户的引用内容,比如引用[2]中提到的阻塞队列的put和take方法使用锁和条件变量,当队列满或空时线程会进入等待状态,避免CPU空转。引用[4]提到LinkedBlockingQueue作为默认选择的原因,可能包括吞吐量和灵活性。需要将这些点整合起来,说明阻塞队列如何帮助线程池管理任务和线程资源,而非阻塞队列的问题,如无法缓冲任务、导致频繁拒绝或资源耗尽。 另外,用户引用[3]提到Tomcat的线程池使用TaskQueue(继承自LinkedBlockingQueue),说明实际应用中的例子,可以引用这个例子来支持线程池使用阻塞队列的常见性。 需要注意用户可能已经了解部分内容,所以需要简明扼要,结构清晰,分点回答。同时,用户提供的引用中有一些源码分析,比如put方法使用await()导致线程进入WAITING状态,这可以用于解释阻塞队列如何工作,避免线程忙等待,提升效率。 可能还需要对比阻塞队列和非阻塞队列的差异,比如非阻塞队列如ConcurrentLinkedQueue在队列满或空时立即返回,导致线程池无法有效处理任务,需要依赖拒绝策略,而阻塞队列能自然地进行流量控制和资源管理。 最后,总结LinkedBlockingQueue作为阻塞队列的地位,以及线程池选择它的原因,包括设计上的协同工作、资源管控和稳定性等。</think>### **LinkedBlockingQueue阻塞队列属性及线程池选择原因** --- #### **一、LinkedBlockingQueue是标准的阻塞队列** 1. **定义特性** - **阻塞队列接口实现**: $LinkedBlockingQueue$ 是 Java 中实现了 $BlockingQueue$ 接口的典型阻塞队列,支持线程安全的**生产者-消费者模型**。其核心方法(如$put()$、$take()$)在队列满或空时自动阻塞线程,符合阻塞队列的核心特征[^1][^4]。 - **数据结构容量**: - 基于链表实现,默认容量为$Integer.MAX\_VALUE$(无界队列),也可通过构造函数指定为有界队列。 - 高并发场景下吞吐量优于$ArrayBlockingQueue$,因采用“双锁分离”设计(生产者和消费者使用独立锁),减少竞争[^1][^3]。 2. **典型应用场景** - **线程池默认队列**: Java 标准线程池(如$Executors.newFixedThreadPool$)默认使用$LinkedBlockingQueue$,因其平衡了吞吐量和任务缓冲能力[^4]。 - **框架级使用**: 如 Tomcat 的线程池通过自定义$TaskQueue$(继承自$LinkedBlockingQueue$)管理任务,优化任务提交执行流程[^3]。 --- #### **二、线程池选择阻塞队列的核心原因** 1. **任务缓冲流量削峰** - **队列容量动态调控**: 阻塞队列(如$LinkedBlockingQueue$)允许线程池在任务量超过$corePoolSize$时暂存任务,避免直接触发扩容或拒绝策略。例如: ```java // 线程池任务提交逻辑(简化) if (当前线程数 < corePoolSize) 创建新线程; else if (队列未满) 任务入队; else if (线程数 < maxPoolSize) 创建新线程; else 执行拒绝策略; ``` - **防止瞬时过载**: 突发流量通过队列缓冲后,线程池可按固定速率处理任务,避免短时高负载压垮系统(如电商秒杀场景)[^4]。 2. **高效的线程协作机制** - **自动阻塞唤醒**: - 生产者线程调用$put()$时,若队列满,则通过$ReentrantLock$和$Condition$进入等待状态(调用$await()$),释放锁并挂起线程,避免空转消耗CPU[^2]。 - 消费者线程调用$take()$时,若队列空,同样挂起等待,直到新任务到达后被唤醒[^1]。 ```java // LinkedBlockingQueue.put()方法逻辑(简化) lock.lock(); try { while (队列满) notFull.await(); // 线程进入WAITING状态 enqueue(task); } finally { lock.unlock(); } ``` - **减少竞争开销**: $LinkedBlockingQueue$的“双锁分离”设计(生产者和消费者使用不同锁),显著降低高并发下的锁争用,提升吞吐量[^3]。 3. **资源稳定性参数协同** - **防止资源耗尽**: 使用有界$LinkedBlockingQueue$可限制任务堆积量,避免无限制增长导致内存溢出(OOM)。队列满后触发扩容或拒绝策略,形成资源保护屏障。 - **动态扩缩容**: 队列容量线程池参数($corePoolSize$, $maxPoolSize$)协同工作: - 队列未满时,优先复用核心线程; - 队列满后,扩容至$maxPoolSize$以应对峰值; - 任务减少后,超时线程自动回收[^4]。 4. **阻塞队列的对比劣势** - **非阻塞队列的缺陷**: - **无法缓冲任务**:如$ConcurrentLinkedQueue$(非阻塞队列)在队列满时直接返回失败,导致线程池频繁触发拒绝策略,即使系统负载仅为短暂峰值。 - **CPU资源浪费**:消费者线程需循环轮询队列(如`while (queue.isEmpty())`),导致空转消耗CPU,而阻塞队列通过挂起线程彻底避免这一问题[^2]。 - **参数失效**:非阻塞队列无法$maxPoolSize$协同,线程池可能始终维持$corePoolSize$线程数,失去弹性扩缩容能力。 --- #### **三、总结** 1. **LinkedBlockingQueue的定位**: 它是标准的阻塞队列,通过链表实现、双锁分离设计,在高并发场景下提供高吞吐量和灵活的任务缓冲能力,是线程池的默认选择之一[^1][^3]。 2. **线程池选择阻塞队列的本质原因**: - **系统稳定性**:通过队列容量限制和阻塞机制,防止资源耗尽; - **高效协作**:自动挂起/唤醒线程,减少CPU空转; - **参数协同**:线程池核心参数动态配合,实现弹性资源管理。 --- ### **相关问题** 1. 如何根据业务场景选择$LinkedBlockingQueue$和$ArrayBlockingQueue$? 2. 为什么$SynchronousQueue$适合“直接传递”任务场景? 3. 如何监控阻塞队列长度以优化线程池性能?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值