【并发编程】基于链表结构实现的一个无界阻塞队列LinkedBlockingQueue

LinkedBlockingQueue是什么

  • LinkedBlockingQueue是一个基于链表实现的阻塞队列。
  • 默认情况下,该阻塞队列的大小为Integer.MAX_VALUE,由于这个数值特别大,所以 LinkedBlockingQueue 也被称作无界队列。
  • 为了避免队列过大造成机器负载或者内存爆满的情况出现,我们在使用的时候建议手动传一个队列的大小。
  • 不指定队列的大小,如果没有剩余内存不足,队列将抛出OOM错误。

LinkedBlockingQueue的实现原理

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

LinkedBlockingQueue的特点

  • 无界阻塞队列,可以指定容量,默认为 Integer.MAX_VALUE,先进先出,存取互不干扰。
  • 使用的数据结构是数组。
  • 可以指定容量,默认为Integer.MAX_VALUE,内部类 Node 存储元素。
  • 锁分离:存取互不干扰,存取操作的是不同的Node对象。删除元素时两个锁一起加!
  • takeLock:取Node节点保证前驱后继不会乱。
  • putLock:存Node节点保证前驱后继不会乱。

LinkedBlockingQueue的入队出队操作

  • 先进先出!
  • 入队阻塞对象notFull:队列count=capacity,放不进去元素时,阻塞在该对象上。
  • 出队阻塞对象notEmpty:队列count=0,无元素可取时,阻塞在该对象上。
  • 入队操作:队尾入队,由last指针记录。
  • 出队操作:队首出队,由head指针记录。

LinkedBlockingQueue与ArrayBlockingQueue对比

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

LinkedBlockingQueue的数据结构源码分析

/** The capacity bound, or Integer.MAX_VALUE if none */
// 链表容量,指定容量就是有界队列
private final int capacity;

/** Current number of elements */
// 元素数量
private final AtomicInteger count = new AtomicInteger();

/**
 * Head of linked list.
 * Invariant: head.item == null
 */
// 链表头:本身是不存储任何元素的,初始化时item指向null
transient Node<E> head;

/**
 * Tail of linked list.
 * Invariant: last.next == null
 */
// 链表尾
private transient Node<E> last;

/** Lock held by take, poll, etc */
// take锁:锁分离,提高效率
private final ReentrantLock takeLock = new ReentrantLock();

/** Wait queue for waiting takes */
// notEmpty条件:当队列无元素时,take锁会阻塞在notEmpty条件上,等待其它线程唤醒
private final Condition notEmpty = takeLock.newCondition();

/** Lock held by put, offer, etc */
// put锁
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
// notFull条件:当队列满了时,put锁会会阻塞在notFull上,等待其它线程唤醒
private final Condition notFull = putLock.newCondition();

// 单链表数据结构
static class Node<E> {
    // 存储的元素
    E item;
    // //后继节点:单链表结构
    Node<E> next;
    // 构造方法,定义当前的节点
    Node(E x) { item = x; }
}

LinkedBlockingQueue的构造方法源码分析

/**
 * 默认无参的构造方法:直接调用队列大小是int最大长度的构造方法
 */
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

/**
 * 指定容量的构造方法
 * capacity:链表的最大长度
 */
public LinkedBlockingQueue(int capacity) {
    // 传入最大的链表长度小于0,直接抛出异常
    if (capacity <= 0) throw new IllegalArgumentException();
    // 给链表的最大长度赋值
    this.capacity = capacity;
    // 初始化一个为null的头尾节点
    last = head = new Node<E>(null);
}

/**
 * 指定初始数据的构造方法
 * c:可以将已经存在的列表初始化到阻塞队列中。
 */
public LinkedBlockingQueue(Collection<? extends E> c) {
    // 调用链表最大长度是int的最大值容量的构造方法
    this(Integer.MAX_VALUE);
    // 获取阻塞队列的内置锁
    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();
    }
}

/**
 * 添加元素到队列中
 */
private void enqueue(Node<E> node) {
    // 尾部插入:之前尾结点的下一个节点是当前节点,并且吧当前节点设置为最新的尾结点!
    last = last.next = node;
}

LinkedBlockingQueue的入队方法:put(E e) 源码分析

/**
 * 往LinkedBlockingQueue中插入元素
 */
public void put(E e) throws InterruptedException {
    // 如果元素是NULL,抛出异常
    if (e == null) throw new NullPointerException();
    // Note: convention in all put/take/etc is to preset local var
    // holding count negative to indicate failure unless set.
    // 定义一个用于接收数量的变量
    int c = -1;
    // 初始化当前的节点
    Node<E> node = new Node<E>(e);
    // 获取当前的put锁
    final ReentrantLock putLock = this.putLock;
    // 得到当前的元素个数
    final AtomicInteger count = this.count;
    // 获取锁:优先考虑中断
    putLock.lockInterruptibly();
    try {
        /*
         * Note that count is used in wait guard even though it is
         * not protected by lock. This works because count can
         * only decrease at this point (all other puts are shut
         * out by lock), and we (or some other waiting put) are
         * signalled if it ever changes from capacity. Similarly
         * for all other uses of count in other wait guards.
         */
        // 达到最大的容量,就阻塞在notFull上等待被其它线程唤醒(阻塞生产者线程)
        while (count.get() == capacity) {
            notFull.await();
        }
        // 队列没有满,入队操作!
        enqueue(node);
        // 原子类加一
        c = count.getAndIncrement();
        // 如果队列还有空闲,准备唤醒被阻塞的生产者线程
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        // 释放锁,真正唤醒生产者线程!
        putLock.unlock();
    }
    // 原队列的长度为0,加了一个元素后立即唤醒阻塞在notEmpty上的线程
    if (c == 0)
        // 唤醒阻塞在notEmpty上的线程
        signalNotEmpty();
}

/**
 * 唤醒阻塞在notEmpty上的线程
 */
private void signalNotEmpty() {
    // 获取当前的take锁
    final ReentrantLock takeLock = this.takeLock;
    // 获取锁
    takeLock.lock();
    try {
        // notEmpty条件转同步队列
        notEmpty.signal();
    } finally {
        // 唤醒消费者线程
        takeLock.unlock();
    }
}

LinkedBlockingQueue的出队方法:take() 源码分析

/**
 * LinkedBlockingQueue的出队方法
 */
public E take() throws InterruptedException {
    // 定义一个当前要出队元素的变量
    E x;
    // 定义一个用于接收数量的变量
    int c = -1;
    // 得到现在链表中的元素数量
    final AtomicInteger count = this.count;
    // 获取take锁
    final ReentrantLock takeLock = this.takeLock;
    // 获取锁操作
    takeLock.lockInterruptibly();
    try {
        // 如果队列无元素,则阻塞在notEmpty条件上(消费者线程阻塞)
        while (count.get() == 0) {
            notEmpty.await();
        }
        // 出队操作
        x = dequeue();
        // 总数量减一
        c = count.getAndDecrement();
        // 如果队列还有元素,唤醒被阻塞的消费着线程
        if (c > 1)
            notEmpty.signal();
    } finally {
        // 真正唤醒消费者线程
        takeLock.unlock();
    }
    // 链表中元素达到最大的时候(为了减少锁的次数)
    if (c == capacity)
        // 唤醒阻塞在notFull上的线程
        signalNotFull();
    // 返回出队的元素
    return x;
}

/**
 * 出队操作
 */
private E dequeue() {
    // assert takeLock.isHeldByCurrentThread();
    // assert head.item == null;
    // 得到投机诶单
    Node<E> h = head;
    // 得到头结点的下一个节点
    Node<E> first = h.next;
    // 取消头节点的指向,方便GC去回收
    h.next = h; // help GC
    // 将下一个节点设置为新的头结点
    head = first;
    // 得到新节点中的元素
    E x = first.item;
    // 将新头节点的元素变为空,头结点不存数据!
    first.item = null;
    // 返回具体的元素
    return x;
}

/**
 * 唤醒阻塞在notFull上的线程
 */
private void signalNotFull() {
    // 得到put锁
    final ReentrantLock putLock = this.putLock;
    // put锁获取锁
    putLock.lock();
    try {
        // 吧notFull条件队列的节点转换到同步队列,准备唤醒阻塞在notFull上的线程
        notFull.signal();
    } finally {
        // 解锁,这才会真正的唤醒生产者线程
        putLock.unlock();
    }
}

结束语

  • 获取更多本文的前置知识文章,以及新的有价值的文章,让我们一起成为架构师!
  • 关注公众号,可以让你对MySQL有非常深入的了解
  • 关注公众号,每天持续高效的了解并发编程!
  • 关注公众号,后续持续高效的了解spring源码!
  • 这个公众号,无广告!!!每日更新!!!
    作者公众号.jpg
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值