特点
- 无界阻塞队列,可以指定容量,默认为 Integer.MAX_VALUE,先进先出,存取互不干扰
- 使用链表的数据结构进行实现
- 锁分离:存取互不干扰,存取操作的是不同的Node对象,存操作(takeLock)的是尾节点,取操作(putLock)的是头节点
- 阻塞队列中实际存储的是数据,条件等待队列中阻塞的是线程
代码
package com.company.blockingqueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* @Author: Alan
* @Date: 2022/11/28 11:23
*/
public class LinkedBlockingQueueDemo {
public static void main(String[] args) {
LinkedBlockingQueue<String> linkedBlockingQueue = new LinkedBlockingQueue<>();
new Thread(()->{
for (int i = 0; i < 10; i++) {
String e = null;
try {
e = linkedBlockingQueue.take();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println("取出元素:" + e);
}
}).start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
String e = String.valueOf(i);
linkedBlockingQueue.put(e);
System.out.println("放入元素:" + e);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}).start();
}
}
put流程
put()操作主要负责把一个对象放入到阻塞队列中(即生产数据)。从源码可以看出来,其主要做了以下4件事儿。
- 根据传入的值,构建一个节点
- 使用尾插法将新节点入队
- 新节点入队后,对count(保存队列中当前值的个数)进行加1,执行count.getAndIncrement()方法调用,会对count加1,然后返回count的旧值。如果此时阻塞队列未达到设定的容量,那么会将条件等待队列中的生产者线程转移同步等待队列中。
- 如果此时c=0,那么说明此时像阻塞队列中投递了一个可以消费的对象,此时可以唤醒消费者线程来进行消费(实质也是先讲消费者先从条件等待队列中转移到同步等待队列中)。
take流程
take()主要负责从阻塞队列中取出一个对象(即消费数据)。从源码可以看出来,其主要做了4件事儿。
-
查看一下count的值,如果为0,即阻塞队列为空,那么阻塞消费者线程(没有数据可以被消费)
-
当第一步的while循环条件不满足时,即阻塞队列中已经有了可以被消费的数据,那么将阻塞队列中的头节点出队(这里给大家提个醒,我们的阻塞队列中实际存储的是数据,条件等待队列中阻塞的是线程,所以这里出队也就是数据)。
在这里我们来看看阻塞队列中,数据的存储情况。未出队前,阻塞队列如下所示
头节点出队后的阻塞对列情况。
-
如果count>1,那么说明原本阻塞队列中的元素大于等于两个,所以这里还可以继续将其他的消费者线程转移到同步队列中,对阻塞队列中的元素进行消费(finally代码块中takeLock释放锁后,同步队列中阻塞的线程便会被唤醒)。
-
如何c自减前等于capacity,经过前面的操作后,此时阻塞队列中肯定会有一个空位,那么就可以唤醒条件等待队列中的生产者线程。这里signalNotFull()方法,实现了将条件队列中阻塞的生产者线程转移到同步阻塞队列,同时唤醒同步阻塞队列中的线程。
remove流程
remove(Object o)操作,负责移除阻塞队列中指定的元素。该方法主要阻要做了4件事儿。
-
首先获取putLock和takeLock,这两把锁分别用于在阻塞队列中put和take元素时,加锁。我们看了上面put和take的流程分析,每次在取、存元素的时候都只是加了一把锁,为啥在移除的时候却要加两把锁呢?
我们来假设一下,假设把remove(Object o)操作想象成是特殊的take()操作,即我们只加takeLock锁,那么也就是说我们在移除元素的时候,是允许有线程新增加元素的。此时我们来看下remove时,元素出队的逻辑(下面的第一张图),if前的逻辑我们先不看(这里便是节点出队),if判断通过后会对last指针进行修改。我们再来看看节点执行put操作时,节点入队的逻辑(下面的第二张图),因为是使用尾插法,所以同样会对last指针进行操作。在这里我们便可以发现,last指针在两个方法中是共享变量,如果两个线程对其同时进行写操作,便会存在线程安全问题。因此,只加一把锁是不够的。
上面在分析remove(Obejct o)操作为啥要加两把锁时,我们从last指针被共享的角度进行了分析,从head指针被共享的角度同样可以得出这个结论。 -
遍历链表,寻找和被移除元素值相同的元素。如果相同,那么通过unlink()方法进行移除,并返回true。这里需要注意,remove(Object e)只会移除一个元素。
-
释放锁,这里和操作1是成对出现的。