LinkedBlockingQueue的实现原理及源码解读

303 阅读6分钟

一、LinkedBlockingQueue

LinkedBlockingQueue是一个基于链表实现的阻塞队列,默认情况下,该阻塞队列的大小为Integer.MAX_VALUE,由于这个数值特别大,所以 LinkedBlockingQueue 也被称作无界队列,代表它几乎没有界限,队列可以随着元素的添加而动态增长,但是如果没有剩余内存,则队列将抛出OOM错误。所以为了避免队列过大造成机器负载或者内存爆满的情况出现,我们在使用的时候建议手动传一个队列的大小。

LinkedBlockingQueue内部由单链表实现,只能从head取元素,从tail添加元素。LinkedBlockingQueue采用两把锁的锁分离技术实现入队出队互不阻塞,添加元素和获取元素都有独立的锁,也就是说LinkedBlockingQueue是读写分离的,读写操作可以并行执行。

image.png

二、LinkedBlockingQueue的使用

//指定队列的大小创建有界队列 
BlockingQueue<Integer> boundedQueue = new LinkedBlockingQueue<>(100); 
//无界队列 
BlockingQueue<Integer> unboundedQueue = new LinkedBlockingQueue<>();

2-2、LinkedBlockingQueue

2-2-1、LinkedBlockingQueue队列的基本结构

// 容量,指定容量就是有界队列
    private final int capacity;
    // 元素数量
    private final AtomicInteger count = new AtomicInteger();
    // 链表头  本身是不存储任何元素的,初始化时item指向null
    transient Node<E> head;
    // 链表尾
    private transient Node<E> last;
    // take锁   锁分离,提高效率
    private final ReentrantLock takeLock = new ReentrantLock();
    // notEmpty条件
// 当队列无元素时,take锁会阻塞在notEmpty条件上,等待其它线程唤醒
    private final Condition notEmpty = takeLock.newCondition();
    // put锁
    private final ReentrantLock putLock = new ReentrantLock();
    // notFull条件
// 当队列满了时,put锁会会阻塞在notFull上,等待其它线程唤醒
    private final Condition notFull = putLock.newCondition();

    //典型的单链表结构
    static class Node<E> {
        E item;  //存储元素
        Node<E> next;  //后继节点    单链表结构
        Node(E x) { item = x; }
    }

2-2-1-1、数据结构

LinkedBlockingQueue底层使用链表的实现方式

可以看到LinkedBlockingQueue中有一个Node内部类,里面有item用于存储数据,next用于指向链表的下一个节点,内部类的构造就是将传入的值给到item。

capacity用于设置链表的大小 image.png

2-2-1-2、put锁和take锁的设置及使用条件队列

下图中:

head设置了头节点,以last设置了尾节点;

同时设置了takeLock作为取数据时候的独占锁,使用putLock作为取数据时候的独占锁;

notEmpty当队列无数据时,用来阻塞取数据的线程,notFull当队列已经满值,用来阻塞写数据的线程。

image.png

2-1-1-3、构造函数

image.png

如上图LinkedBlockingQueue有三个构造函数,先看前两个

第一个:可以初始化一个无界队列

第二个:可以设置一个指定大小的又接队列,同时设置了一个Node节点,并且头尾都是当前空节点,以下图来说

image.png

第三个:可以将一个集合数据在初始化的时候进行入队操作,

image.png

2-2-2、数据的入队及出队

2-2-2-1、put入队操作

public void put(E e) throws InterruptedException {
    // 不允许null元素
    if (e == null) throw new NullPointerException();
    int c = -1;
    // 新建一个节点
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    // 使用put锁加锁
    putLock.lockInterruptibly();
    try {
        // 如果队列满了,就阻塞在notFull上等待被其它线程唤醒(阻塞生产者线程)
        while (count.get() == capacity) {
            notFull.await();
        }
        // 队列不满,就入队
        enqueue(node);
        c = count.getAndIncrement();// 队列长度加1,返回原值
        // 如果现队列长度小于容量,notFull条件队列转同步队列,准备唤醒一个阻塞在notFull条件上的线程(可以继续入队) 
        // 这里为啥要唤醒一下呢?
        // 因为可能有很多线程阻塞在notFull这个条件上,而取元素时只有取之前队列是满的才会唤醒notFull,此处不用等到取元素时才唤醒
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        putLock.unlock(); // 真正唤醒生产者线程
    }
    // 如果原队列长度为0,现在加了一个元素后立即唤醒阻塞在notEmpty上的线程
    if (c == 0)
        signalNotEmpty();
}
private void enqueue(Node<E> node) {
    // 直接加到last后面,last指向入队元素
    last = last.next = node;
}
private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();// 加take锁
    try {
        notEmpty.signal();// notEmpty条件队列转同步队列,准备唤醒阻塞在notEmpty上的线程
    } finally {
        takeLock.unlock();  // 真正唤醒消费者线程
    }

2-2-2-2、take出队操作

public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    // 使用takeLock加锁
    takeLock.lockInterruptibly();
    try {
        // 如果队列无元素,则阻塞在notEmpty条件上(消费者线程阻塞)
        while (count.get() == 0) {
            notEmpty.await();
        }
        // 否则,出队
        x = dequeue();
        c = count.getAndDecrement();//长度-1,返回原值
        if (c > 1)// 如果取之前队列长度大于1,notEmpty条件队列转同步队列,准备唤醒阻塞在notEmpty上的线程,原因与入队同理
            notEmpty.signal();
    } finally {
        takeLock.unlock(); // 真正唤醒消费者线程
    }
    // 为什么队列是满的才唤醒阻塞在notFull上的线程呢?
    // 因为唤醒是需要加putLock的,这是为了减少锁的次数,所以,这里索性在放完元素就检测一下,未满就唤醒其它notFull上的线程,
    // 这也是锁分离带来的代价
    // 如果取之前队列长度等于容量(已满),则唤醒阻塞在notFull的线程
    if (c == capacity)
        signalNotFull();
    return x;
}
private E dequeue() {
    // head节点本身是不存储任何元素的
    // 这里把head删除,并把head下一个节点作为新的值
    // 并把其值置空,返回原来的值
    Node<E> h = head;
    Node<E> first = h.next;
    h.next = h; // 方便GC
    head = first;
    E x = first.item;
    first.item = null;
    return x;
}
private void signalNotFull() {
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        notFull.signal();// notFull条件队列转同步队列,准备唤醒阻塞在notFull上的线程
    } finally {
        putLock.unlock(); // 解锁,这才会真正的唤醒生产者线程
    }

2-2-3、LinkedBlockingQueue与ArrayBlockingQueue对比

LinkedBlockingQueue是一个阻塞队列,内部由两个ReentrantLock来实现出入队列的线程安全,由各自的Condition对象的await和signal来实现等待和唤醒功能。它和ArrayBlockingQueue的不同点在于:

  • 队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
  • 数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。
  • 由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。
  • 两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

三、LinkedBlockingDeque

和LinkedBlockingQueue类似的LinkedBlockingDeque也是链表的方式,LinkedBlockingDeque和LinkedBlockingQueue主要区别就是:

1、入队出队的操作,从队首和队尾都可以,

2、LinkedBlockingQueue的存取分别使用各自的锁有两把锁,而LinkedBlockingDeque使用的是一把锁,同一时间要么读要么写

image.png