集合(八) -- 阻塞队列ArrayBlockingQueue和LinkedBlockingQueue

1. 阻塞队列:

阻塞队列与我们平常接触的普通队列(LinkedList或ArrayList等)的最大不同点,在于阻塞队列的阻塞添加(入队)和阻塞删除(出队)方法。

  • 阻塞添加:
    所谓的阻塞添加是指当阻塞队列元素已满时,队列会阻塞加入元素的线程,直队列元素不满时才重新唤醒线程执行元素加入操作。
  • 阻塞删除:
    阻塞删除是指在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般都会返回被删除的元素)

实现方式:

1.1 ArrayBlockingQueue

用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。

1.2 LinkedBlockingQueue

基于链表的阻塞队列,同ArrayListBlockingQueue类似,此队列按照先进先出(FIFO)的原则对元素进行排序,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

这里以LinkedBlockingQueue的实现举例讲解阻塞队列:

2. LinkedBlockingDeque:

2.1 原理解析:

LinkedBlockingQueue内部由单链表实现,只能从head取元素,从tail添加元素。添加元素和获取元素都有独立的锁,也就是说LinkedBlockingQueue是读写分离的,读写操作可以并行执行。

LinkedBlockingQueue采用可重入锁(ReentrantLock)来保证在并发情况下的线程安全。

2.2 构造器:

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

public LinkedBlockingQueue()//设置容量为Integer.MAX
 
public LinkedBlockingQueue(int capacity)//设置指定容量

public LinkedBlockingQueue(Collection<? extends E> c)//穿入一个容器,如果调用该构造器,容量默认也是Integer.MAX_VALUE

2.3 成员变量:

//队列中元素个数,用于控制阻塞
private final AtomicInteger count = new AtomicInteger();
//头节点
transient Node<E> head;
//尾节点
private transient Node<E> last;
//出队锁
private final ReentrantLock takeLock = new ReentrantLock();
//如果队列为空,出队就会陷入等待
private final Condition notEmpty = takeLock.newCondition();
//入队锁
private final ReentrantLock putLock = new ReentrantLock();
//如果队列满了,入队就陷入等待
private final Condition notFull = putLock.newCondition();

2.4 内部维护的节点对象:

public class LinkedBlockingDeque<E>
    extends AbstractQueue<E>
    implements BlockingDeque<E>, java.io.Serializable {
	 /** Doubly-linked list node class */
    static final class Node<E> {
        /**
         * The item, or null if this node has been removed.
         */
        E item;

		// JDK13中的,jdk8没有
        /**
         * One of:
         * - the real predecessor Node
         * - this Node, meaning the predecessor is tail
         * - null, meaning there is no predecessor
         */
        // Node<E> prev;

        /**
         * One of:
         * - the real successor Node
         * - this Node, meaning the successor is head
         * - null, meaning there is no successor
         */
        /**
         * 下列三种情况之
         * - 真正的后继节点
         * - 自己,发生在出列时
         * - null, 表示是没有后继节点,是最后了
        /
        Node<E> next;

        Node(E x) {
            item = x;
        }
    }
}

2.5 主要方法:

2.5.1 取数据
  • remove(Object o) :移除指定元素,成功返回true,失败返回false
  • poll() : 获取并移除此队列的头元素,若队列为空,则返回 null
  • take():获取并移除此队列头元素,若没有元素则一直阻塞。
2.5.2 添加数据
  • add(E e) : 添加成功返回true,失败抛IllegalStateException异常
  • offer(E e) : 成功返回 true,如果此队列已满,则返回 false。
  • put(E e) :将元素插入此队列的尾部,如果该队列已满,则一直阻塞
2.5.3 判断队列是否为空
  • size()方法会遍历整个队列,时间复杂度为O(n),所以最好选用isEmtpy
2.5.4 检查方法
  • element() :获取但不移除此队列的头元素,没有元素则抛异常
  • peek() :获取但不移除此队列的头;若队列为空,则返回 null。

2.6 初始化方法:

//传入队列大小
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    //head为头节点,last为尾节点
    //初始化为null,可以看出当queue为空的时候,head==tail
    last = head = new Node<E>(null);
}
//默认的构造方法
public LinkedBlockingQueue() {
    //队列大小为Integer.MAX_VALUE
    this(Integer.MAX_VALUE);
}

2.7 添加方法:

2.7.1 阻塞添加put方法:
public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    
    int c = -1;
    Node<E> node = new Node<E>(e);
    //利用ReentrantLock独占锁来加锁,保证同时只有一个线程来put
    final ReentrantLock putLock = this.putLock;
    //利用AtomicInteger来表示queue中的元素个数
    final AtomicInteger count = this.count;
    //可打断的加锁
    putLock.lockInterruptibly();
    try {
		// private final Condition notFull = putLock.newCondition();
        //如果队列满了,就调用notFull。await()。notFull是putLock的条件变量,当调用notFull.await()会将putLock释放,阻塞在等待队列notFull上
        while (count.get() == capacity) {
            notFull.await();
        }

        //入队,不用获得takeLock,因为与出队操作不涉及共享变量
        //从入队代码可以看出head是一个哨兵节点,不存放任何实际数据
        //last = last.next = node;
        enqueue(node);

        //count++
        c = count.getAndIncrement();
        //如果队列未满,唤醒被阻塞的入队线程
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    //如果c == 0,说明入队之前队列为空,唤醒出队的等待线程
    if (c == 0)
        signalNotEmpty();
}

private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    //获取出队锁
    takeLock.lock();
    try {
        //唤醒出队等待线程
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}

2.7.2 非阻塞添加offer方法
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 {
        //和put()不同的是,当队列满的时候,offer()直接返回false,不会阻塞
        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;
}

2.8 获取方法:

2.8.1 阻塞获取take方法
public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        //如果队列为空,放弃takeLock,阻塞在等待队列notEmpty上
        while (count.get() == 0) {
            notEmpty.await();
        }
        //出队
        x = dequeue();
        //count--;
        c = count.getAndDecrement();
        //如果队列不为空,唤醒出队等待线程
        if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    //如果队列不为空,唤醒入队等待线程
    if (c == capacity)
        signalNotFull();
    return x;
}

private E dequeue() {
    //head是哨兵节点,不存放数据,实际的头节点是head.next
    Node<E> h = head;
    //head的next
    Node<E> first = h.next;
    h.next = h;
    //将head踢出
    head = first;
    //first的item才是第一个元素,head是哨兵节点
    E x = first.item;
    first.item = null;
    //从dequeue方法可以看出,queue中始终有一个哨兵head节点,不存储任何数据,queue中第一个元素是head.next
    return x;
}

private void signalNotFull() {
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        //唤醒入队等待线程
        notFull.signal();
    } finally {
        putLock.unlock();
    }
}

2.8.2 非阻塞获取poll方法
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 {
        //和take()方法不同的是,如果队列为空,就直接返回null,不会阻塞
        if (count.get() > 0) {
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        }
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

2.9 获取队列头但是不出队方法peek:

public E peek() {
    //如果队列为空,返回null
    if (count.get() == 0)
        return null;
    //获取出队锁,防止在peek()期间由其他线程执行出队操作
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        //head是哨兵节点,head.next才是第一个节点
        Node<E> first = head.next;
        if (first == null)
            return null;
        else
            return first.item;
    } finally {
        takeLock.unlock();
    }
}

2.10 入队图示:

2.10.1 初始化链表:

last = head = new Node< E >(null);Dummy节点(哨兵节点)来占位(last和head都指向dummy节点),item为null
在这里插入图片描述

2.10.2 第一个节点入队:

last = last.next = node;
把新节点赋值为当前last节点的后节点,并把last节点设置为当前节点
在这里插入图片描述

2.10.3 第二个及之后的节点入队:

last = last.next = node;

在这里插入图片描述

2.11 出队图示:

Node<E> h = head;
Node<E> first = h.next;
h.next = h;
head = first;
E x = first.item;
first.item = null;
return x;
2.11.1 Node h = head

将临时变量h赋值给头节点
在这里插入图片描述

2.11.2 first = h.next;

将临时变量first赋值给h的下一个
在这里插入图片描述

2.11.3 h.next = h

将h的next指向自己,目的是不让next指向其他节点,保证此节点能安全的被垃圾回收

在这里插入图片描述

2.11.4 head = first

在这里插入图片描述

2.11.5
E x = first.item;
first.item = null;
return x;

在这里插入图片描述

2.12 加锁分析:

2.12.1 高明之处在于加了2把锁:
  • 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行
  • 用两把锁,同一时刻可以允许2个线程同时(一个生产者一个消费者)执行
    • 消费者与消费者线程仍然串行
    • 生产者与生产者线程仍然串行
2.12.2 线程安全分析:
  • 当节点总数大于2时(包括dummy节点),putLock保证的是last节点的线程安全,takeLock保证的是head节点的线程安全。两把锁保证了入队和出队没有竞争
  • 当节点总数等于2时(即一个dummy节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争
  • 当节点总数等于1时(就一个dummy节点),这时take线程会被noteEmpty条件阻塞,有竞争,会阻塞
// 用于put(阻塞), offer(非阻塞)
private final ReentrantLock putLock = new ReentrantLock();
// 用于take(阻塞),poll(非阻塞)
private final ReentrantLock takeLock = new ReentrantLock();

2.12 和Array性能比较:

主要列举LinkedBlockingQueue(推荐)和ArrayBlockingQueue的性能比较

  • Linked支持有界,Array强制有界
  • Linked实现是链表,Array是数组
  • Linked是懒惰的,而Array需要提前初始化Node数组
  • Linked每次入队会生成新Node,而Array的Node是提前创建好的
  • Linked两把锁,Array一把锁

3. LinkedBlockingQueue和ArrayBlockingQueue的不同点在于:

3.1 锁机制不同

  • LinkedBlockingQueue中的锁是分离的,生产者的锁PutLock,消费者的锁takeLock。这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能
  • 而ArrayBlockingQueue生产者和消费者使用的是同一把锁;

3.2 底层实现机制也不同

  • LinkedBlockingQueue内部维护的是一个链表结构。
    在生产和消费的时候,需要创建Node对象进行插入或移除,大批量数据的系统中,其对于GC的压力会比较大。
  • 而ArrayBlockingQueue内部维护了一个数组
    在生产和消费的时候,是直接将枚举对象插入或移除的,不会产生或销毁任何额外的对象实例。

3.3 构造时候的区别

  • LinkedBlockingQueue有默认的容量大小为:Integer.MAX_VALUE,所以有可能会造成内存溢出的问题,当然也可以传入指定的容量大小
  • ArrayBlockingQueue在初始化的时候,必须传入一个容量大小的值

3.4 执行clear()方法

  • LinkedBlockingQueue执行clear方法时,会加上两把锁

4. ConcurrentLinkedQueue:

ConcurrentLinkedQueue的设计和LinkedBlockingQueue非常像,也是:

  • 两把锁,同一时刻,可以允许两个线程(一个生产者一个消费者)执行
  • dummy节点的引入让两把锁将来锁住的是不同对象,避免竞争
  • 只是这锁使用了cas来实现

事实上,ConcurrentLinkedQueue应用还是非常广泛的:
例如之前讲的Tomcat的Connector结构时,Acceptor作为生产者向Poller消费者传递事件信息时,正是采用了ConcurrentLinkedQueue将ScoketChannel给Poller使用:
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值