1、五种阻塞队列介绍

  • ArrayBlockingQueue

    有界队列,底层使用数组实现,并发控制使用ReentrantLock控制,不管是插入操作还是读取操作,都需要获取锁之后才能执行。
  • LinkedBlockingQueue

    底层基于单向链表实现,既可以当做有界队列,也可以当做无界队列使用。使用两个ReentrantLock实现并发控制:takelock和putlock。
  • SynchronousQueue

    底层使用单向链表实现,只有一个元素,同步的意思是一个写操作必须等到一个读操作之后才返回,指的是读写线程的同步。
  • PriorityBlockingQueue

    带排序的阻塞队列的实现,使用数组进行实现。并发控制使用ReentrantLock,队列为无界队列。

    有初始化参数指定队列大小,但是会自动扩容。使用最小堆来实现排序。
  • DelayedQueue

    DelayedQueue是使用PriorityBlockingQueue和Delayed实现的,内部定义了一个优先级队列,当调用offer的时候,把Delayed对象加入队列中,使用take先把first对象拿出来(peek),如果没有到达阈值,进行await处理。

2、poll和peek的区别

都用于取队列的头结点,poll会删除头结点,peek不会删除头结点。

3、LinkedBlockingQueue

  • 是先进先出队列FIFO。
  • 采用ReentrantLock保证线程安全

3.1、功能

3.1.1、增加

增加有三种方式,前提:队列满

方式 put add offer
特点 一直阻塞 抛异常 返回false
3.1.2、删除

删除有三种方式,前提:队列为空

方式 remove poll take
特点 NoSuchElementException 返回false 阻塞

3.2、简单分析

  • LinkedBlockingQueue是一个阻塞队列,内部由两个ReentrantLock来实现出入队列的线程安全,由各自的Condition对象的await和signal来实现等待和唤醒功能。
  • 基于单向链表的、范围任意的(其实是有界的)、FIFO 阻塞队列。
  • 头结点和尾结点一开始总是指向一个哨兵的结点,它不持有实际数据,当队列中有数据时,头结点仍然指向这个哨兵,尾结点指向有效数据的最后一个结点。这样做的好处在于,与计数器 count 结合后,对队头、队尾的访问可以独立进行,而不需要判断头结点与尾结点的关系。

3.2.1、节点与属性
//链表节点内部类
static class Node<E> {
//节点元素
E item;
Node<E> next;
Node(E x) {
item = x;
}
}
//容量界限,如果未设定,则为Integer最大值
private final int capacity;
//当前元素个数
private final AtomicInteger count = new AtomicInteger();
//链表的头:head.item == null
transient Node<E> head;
//链表的尾:last.next == null
private transient Node<E> last;
//take,poll等获取锁
private final ReentrantLock takeLock = new ReentrantLock();
//等待任务的等待队列
private final Condition notEmpty = takeLock.newCondition();
//put,offer等插入锁
private final ReentrantLock putLock = new ReentrantLock();
//等待插入的等待队列
private final Condition notFull = putLock.newCondition();
3.2.2、插入线程与获取线程的相互通知

signalNotEmpty()方法,在插入线程发现队列为空时调用,告知获取线程需要等待。

signalNotFull()方法,在获取线程发现队列已满时调用,告知插入线程需要等待。

//表示等待take。put/offer调用,否则通常不会锁定takeLock。
private void signalNotEmpty() {
//获取takeLock
final ReentrantLock takeLock = this.takeLock;
//锁定takeLock
takeLock.lock();
try {
//唤醒take线程等待队列
notEmpty.signal();
} finally {
//释放锁
takeLock.unlock();
}
}
//表示等待put,take/poll 调用
private void signalNotFull() {
//获取putLock
final ReentrantLock putLock = this.putLock;
//锁定putLock
putLock.lock();
try {
//唤醒插入线程等待队列
notFull.signal();
} finally {
//释放锁
putLock.unlock();
}
}
3.2.3、入队与出队操作

enqueue()方法只能在持有 putLock 锁下执行,dequeue()在持有 takeLock 锁下执行。

//在队列尾部插入
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
//last.next指向当前node
//尾指针后移
last = last.next = node;
}
//移除队列头
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
//保存头指针
Node<E> h = head;
//获取当前链表第一个元素
Node<E> first = h.next;
//头指针的next指向自己
h.next = h; // help GC
//头指针指向第一个元素
head = first;
//获取第一个元素的值
E x = first.item;
//将第一个元素的值置空
first.item = null;
//返回第一个元素的值
return x;
}
3.2.4、对两把锁的加锁与释放

在需要对两把锁同时加锁时,把加锁的顺序与释放的顺序封装成方法,确保所有地方都是一致的。而且获取锁时都是不响应中断的,一直获取直到加锁成功,这就避免了第一把锁加锁成功,而第二把锁加锁失败导致锁不释放的风险。

//锁定putLock和takeLock
void fullyLock() {
putLock.lock();
takeLock.lock();
}
//与fullyLock的加锁顺序相反,先解锁takeLock,再解锁putLock
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}

3.3、源码解读

简单介绍一下LinkedBlockingQueue中API的源码,如构造方法,新增,获取,删除,drainTo。

3.3.1、构造函数

LinkedBlockingQueue有三个构造方法,其中无参构造尽量少用,因为容量为Integer的最大值,操作不当会出现内存溢出。

public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
//参数校验
if (capacity <= 0) throw new IllegalArgumentException();
//设置容量
this.capacity = capacity;
//首尾节点指向一个空节点
last = head = new Node<E>(null);
}
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
//获取putLock
final ReentrantLock putLock = this.putLock;
//锁定
putLock.lock(); // Never contended, but necessary for visibility
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
3.3.2、offer(E e)

将给定的元素设置到队列中,如果设置成功返回true, 否则返回false。 e的值不能为空,否则抛出空指针异常。

//如果可以在不超过队列容量的情况下立即插入指定的元素到队列的尾部,成功后返回true,如果队列已满,返回false。当使用容量受限的队列时,此方法通常比方法BlockingQueue#add更可取,后者只能通过抛出异常才能插入元素。
public boolean offer(E e) {
//非空判断
if (e == null) throw new NullPointerException();
//计数器
final AtomicInteger count = this.count;
//如果队列已满,直接返回插入失败
if (count.get() == capacity)
return false;
int c = -1;
//新建节点
Node<E> node = new Node<E>(e);
//获取插入锁
final ReentrantLock putLock = this.putLock;
//锁定
putLock.lock();
try {
//如果队列未满
if (count.get() < capacity) {
//插入队列
enqueue(node);
//计数
c = count.getAndIncrement();
//还有空余空间
if (c + 1 < capacity)
//唤醒插入线程
notFull.signal();
}
} finally {
//解锁
putLock.unlock();
}
//如果队列为空
if (c == 0)
//通知获取线程阻塞
signalNotEmpty();
//返回成功或者插入失败
return c >= 0;
}
3.3.3、put(E e)

将元素设置到队列中,如果队列中没有多余的空间,该方法会一直阻塞,直到队列中有多余的空间。

public void put(E e) throws InterruptedException {
//不可以插入空元素
if (e == null) throw new NullPointerException(); //所有put/take/etc中的约定都是预先设置本地var
//除非设置,否则保持计数为负数表示失败。
int c = -1;
//新建节点
Node<E> node = new Node<E>(e);
//获取putLock
final ReentrantLock putLock = this.putLock;
//获取计数器
final AtomicInteger count = this.count;
//可中断加锁,即在锁获取过程中不处理中断状态,而是直接抛出中断异常,由上层调用者处理中断。
putLock.lockInterruptibly();
try {
/*
* 注意count在wait守卫线程中使用,即使它没有被锁保护。
* 这是因为count只能在此时减少(所有其他put都被锁定关闭),
* 如果它从容量更改,我们(或其他一些等待put)将收到信号。
* 类似地,count在其他等待守卫线程中的所有其他用途也是如此。
*/
//只要当前队列已满
while (count.get() == capacity) {
//通知插入线程等待
notFull.await();
}
//插入队列
enqueue(node);
//数量加1
c = count.getAndIncrement();
//如果队列增加1个元素还未满
if (c + 1 < capacity)
//唤醒插入进程
notFull.signal();
} finally {
//解锁
putLock.unlock();
}
//如果队列中没有元素了
if (c == 0)
//通知获取线程等待
signalNotEmpty();
}
3.3.4、peek()

非阻塞的获取队列中的第一个元素,不出队列。

public E peek() {
//队列为空,直接返回
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
//获取第一个元素,非哨兵
Node<E> first = head.next;
//元素为空,返回null
if (first == null)
return null;
else
//返回第一个元素值
return first.item;
} finally {
takeLock.unlock();
}
}
3.3.5、poll()

非阻塞的获取队列中的值,未获取到返回null。

public E poll() {
final AtomicInteger count = this.count;
//队列为空,直接返回
if (count.get() == 0)
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
//队列非空,获取队列中元素
if (count.get() > 0) {
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
3.3.6、remove(Object o)

从队列中移除指定的值。将两把锁都锁定。

public boolean remove(Object o) {
//不支持null
if (o == null) return false;
//锁定两个锁
fullyLock();
try {
//迭代队列
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
//通过equals方法匹配待删除元素
if (o.equals(p.item)) {
//移除p节点
unlink(p, trail);
//成功
return true;
}
}
//失败
return false;
} finally {
//解锁
fullyUnlock();
}
}
// 将内部节点p与前一个跟踪断开连接
void unlink(Node<E> p, Node<E> trail) {
// assert isFullyLocked();
// p.next is not changed, to allow iterators that are
// traversing p to maintain their weak-consistency guarantee.
//p节点内容置空
p.item = null;
//trail节点的next指向p的next
trail.next = p.next;
//如果p是队尾
if (last == p)
//trail变为队尾
last = trail;
//如果队列已满
if (count.getAndDecrement() == capacity)
//通知插入线程阻塞
notFull.signal();
}
3.3.7、clear()

清空队列。

//原子性地从队列中删除所有元素。此调用返回后,队列将为空。
public void clear() {
//锁定
fullyLock();
try {
//清空数据,帮助垃圾回收
for (Node<E> p, h = head; (p = h.next) != null; h = p) {
h.next = h;
p.item = null;
}
head = last;
// assert head.item == null && head.next == null;
//如果容量为0
if (count.getAndSet(0) == capacity)
//唤醒插入线程
notFull.signal();
} finally {
//解锁
fullyUnlock();
}
}
3.3.8、drainTo(Collection c)

将队列中值,全部移除,并发设置到给定的集合中。

public int drainTo(Collection<? super E> c, int maxElements) {
//各种判断
if (c == null)
throw new NullPointerException();
if (c == this)
throw new IllegalArgumentException();
if (maxElements <= 0)
return 0;
boolean signalNotFull = false;
//锁
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
//获取要转移的数量
int n = Math.min(maxElements, count.get());
// count.get provides visibility to first n Nodes
Node<E> h = head;
int i = 0;
try {
//组装集合
while (i < n) {
Node<E> p = h.next;
c.add(p.item);
p.item = null;
h.next = h;
h = p;
++i;
}
return n;
} finally {
// Restore invariants even if c.add() threw
if (i > 0) {
// assert h.item == null;
head = h;
signalNotFull = (count.getAndAdd(-i) == capacity);
}
}
} finally {
takeLock.unlock();
if (signalNotFull)
signalNotFull();
}
}

面试【JAVA基础】阻塞队列的更多相关文章

  1. Java基础--阻塞队列ArrayBlockingQueue

    ArrayBlockingQueue是阻塞队列的一种,基于数组实现,长度固定,队尾添加,队首获取, 构造函数: ArrayBlockingQueue(int capacity) ArrayBlocki ...

  2. Java多线程 阻塞队列和并发集合

    转载:大关的博客 Java多线程 阻塞队列和并发集合 本章主要探讨在多线程程序中与集合相关的内容.在多线程程序中,如果使用普通集合往往会造成数据错误,甚至造成程序崩溃.Java为多线程专门提供了特有的 ...

  3. Java集合--阻塞队列及各种实现的解析

    阻塞队列(Blocking Queue) 一.队列的定义 说的阻塞队列,就先了解下什么是队列,队列也是一种特殊的线性表结构,在线性表的基础上加了一条限制:那就是一端入队列,一端出队列,且需要遵循FIF ...

  4. Java:阻塞队列

    Java:阻塞队列 本笔记是根据bilibili上 尚硅谷 的课程 Java大厂面试题第二季 而做的笔记 1. 概述 概念 队列 队列就可以想成是一个数组,从一头进入,一头出去,排队买饭 阻塞队列 B ...

  5. [Java 基础] 并发队列ConcurrentLinkedQueue和阻塞队列LinkedBlockingQueue用法

    reference : http://www.cnblogs.com/linjiqin/archive/2013/05/30/3108188.html 在Java多线程应用中,队列的使用率很高,多数生 ...

  6. JAVA可阻塞队列-ArrayBlockingQueue

    在前面的的文章,写了一个带有缓冲区的队列,是用JAVA的Lock下的Condition实现的,但是JAVA类中提供了这项功能,就是ArrayBlockingQueue, ArrayBlockingQu ...

  7. java 可伸缩阻塞队列实现

    最近一年多写的最虐心的代码.必须好好复习java并发了.搞了一晚上终于测试都跑通过了,特此纪念,以资鼓励! import java.util.ArrayList; import java.util.L ...

  8. java 多线程阻塞队列 与 阻塞方法与和非阻塞方法

    Queue是什么 队列,是一种数据结构.除了优先级队列和LIFO队列外,队列都是以FIFO(先进先出)的方式对各个元素进行排序的.无论使用哪种排序方式,队列的头都是调用remove()或poll()移 ...

  9. Java -- 使用阻塞队列(BlockingQueue)控制线程通信

    BlockingQueeu接口是Queue的子接口,但是它的主要作用并不是作为容器,而是作为线程同步的工具. 特征: 当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程 ...

  10. Java并发--阻塞队列

    在前面几篇文章中,我们讨论了同步容器(Hashtable.Vector),也讨论了并发容器(ConcurrentHashMap.CopyOnWriteArrayList),这些工具都为我们编写多线程程 ...

随机推荐

  1. iptables看门狗

    近来业内很多服务器因redis造成服务器被黑,这个攻击的防范重点在于防火墙!! 有时为了方便我们可能会将iptables临时关闭,方便完倘若忘记把它打开,黑客大摇大摆就走进来. 这时候,我们需要条看门 ...

  2. Docker初探之常用命令

    在正式使用Docker之前,我们先来熟悉下Docker中常用的命令,因为对Docker的操作就如同操作Linux一样,大部分操作通过命令完成. 一.登录 为什么要使用登录? 因为我们使用Docker, ...

  3. Python datetime 转 JSON

    Python datetime 转 JSON Python 中将 datetime 转换为 JSON 类型,在使用 Django 时遇到的问题. 环境: Python2.7 代码: import js ...

  4. ArrayList继承关系分析

    目录 继承关系 Iterable Collection List AbstractCollection AbstractList RandomAccess Serializable Cloneable ...

  5. 网易云音乐ncm格式分析以及ncm与mp3格式转换

    目录 NCM格式分析 音频知识简介 两种可能 GitHub项目 格式分析 总体结构 密钥问题 代码分析 main函数 导入模块 dump函数 参考资料 代码完整版 转换工具 ncmdump ncmdu ...

  6. 如何有效防止sql注入

    SQL注入攻击是黑客对数据库进行攻击常用的手段之一,随着B/S模式应用开发的发展,使用这种模式编写应用程序的程序员也越来越多.但是由于程序员的水平及经验参差不齐,相当大一部分程序员在编写代码的时候,没 ...

  7. 设计模式:单例模式介绍及8种写法(饿汉式、懒汉式、Double-Check、静态内部类、枚举)

    一.饿汉式(静态常量) 这种饿汉式的单例模式构造的步骤如下: 构造器私有化:(防止用new来得到对象实例) 类的内部创建对象:(因为1,所以2) 向外暴露一个静态的公共方法:(getInstance) ...

  8. 总结关于Ubuntu 安装 Docker 配置相关问题及解决方法

    总结关于Ubuntu 安装 Docker 配置相关问题及解决方法 Tomcat 示例 软件镜像(xx安装程序)----运行镜像----产生一个容器(正在运行的软件,运行的xx): 步骤: 1.搜索镜像 ...

  9. 【算法•日更•第四十二期】离散傅里叶变换(DFT)

    ▎前言 小编相当的菜,这篇博客难度稍高,所以有些可能不会带有证明,博客中更多的是定义. 我们将要学到的东西: 复数 暴力多项式乘法 DFT 当然,小编之前就已经写过一篇博客了,主要讲的就是基础多项式, ...

  10. Android 获取对象列表中的某一列 / 所有对象的某一字段,Realm数据库可获取某一字段所有值

    现在项目用的数据库是Realm,所以想要获取数据库中某一字段的数据没有一句直接的语句进行获取,就像MySQL一样的select name from User,从User表里获取所有的name. 所以只 ...