[学习笔记-Java集合-16] Queue - LinkedBlockingQueue源码分析

介绍

LinkedBlockingQueue是java并发包下一个以单链表实现的阻塞队列,它是线程安全的,至于它是否是有界的,请看下面的分析。java

源码分析

主要属性

// 容量
private final int capacity;

// 元素数量
private final AtomicInteger count = new AtomicInteger();

// 链表头
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();

// 放锁
private final ReentrantLock putLock = new ReentrantLock();

// notFull条件
// 当队列满了时,put锁会会阻塞在notFull上,等待其它线程唤醒
private final Condition notFull = putLock.newCondition();
  1. capacity,有容量,能够理解为LinkedBlockingQueue是有界队列
  2. head, last,链表头、链表尾指针
  3. takeLock,notEmpty,take锁及其对应的条件
  4. putLock, notFull,put锁及其对应的条件
  5. 入队、出队使用两个不一样的锁控制,锁分离,提升效率

内部类

典型的单链表结构。node

static class Node<E> {
    E item;

    Node<E> next;

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

主要构造方法

public LinkedBlockingQueue() {
    // 若是没传容量,就使用最大int值初始化其容量
    this(Integer.MAX_VALUE);
}

public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    // 初始化head和last指针为空值节点
    last = head = new Node<E>(null);
}

入队

入队一样有四个方法,咱们这里只分析最重要的一个,put(E e)方法:安全

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);
        // 队列长度加1
        c = count.getAndIncrement();
        // 若是现队列长度若是小于容量
        // 就再唤醒一个阻塞在notFull条件上的线程
        // 这里为啥要唤醒一下呢?
        // 由于可能有不少线程阻塞在notFull这个条件上的
        // 而取元素时只有取以前队列是满的才会唤醒notFull
        // 为何队列满的才唤醒notFull呢?
        // 由于唤醒是须要加putLock的,这是为了减小锁的次数
        // 因此,这里索性在放完元素就检测一下,未满就唤醒其它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.next = node;
}    

private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    // 加take锁
    takeLock.lock();
    try {
        // 唤醒notEmpty条件
        notEmpty.signal();
    } finally {
        // 解锁
        takeLock.unlock();
    }
}
  1. 使用putLock加锁;
  2. 若是队列满了就阻塞在notFull条件上;
  3. 不然就入队;
  4. 若是入队后元素数量小于容量,唤醒其它阻塞在notFull条件上的线程;
  5. 释放锁;
  6. 若是放元素以前队列长度为0,就唤醒notEmpty条件;

出队

出队一样也有四个方法,咱们这里只分析最重要的那一个,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,则唤醒notEmpty
        if (c > 1)
            notEmpty.signal();
    } finally {
        // 释放锁
        takeLock.unlock();
    }
    // 若是取以前队列长度等于容量
    // 则唤醒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; // help GC
    head = first;
    E x = first.item;
    first.item = null;
    return x;
}

private void signalNotFull() {
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        // 唤醒notFull
        notFull.signal();
    } finally {
        putLock.unlock();
    }
}
  1. 使用takeLock加锁;
  2. 若是队列空了就阻塞在notEmpty条件上;
  3. 不然就出队;
  4. 若是出队前元素数量大于1,唤醒其它阻塞在notEmpty条件上的线程;
  5. 释放锁;
  6. 若是取元素以前队列长度等于容量,就唤醒notFull条件;

总结

  1. LinkedBlockingQueue采用单链表的形式实现;
  2. LinkedBlockingQueue采用两把锁的锁分离技术实现入队出队互不阻塞;
  3. LinkedBlockingQueue是有界队列,不传入容量时默认为最大int值;

LinkedBlockingQueue与ArrayBlockingQueue对比?

  1. 后者入队出队采用一把锁,致使入队出队相互阻塞,效率低下;
  2. 前才入队出队采用两把锁,入队出队互不干扰,效率较高;
  3. 两者都是有界队列,若是长度相等且出队速度跟不上入队速度,都会致使大量线程阻塞;
  4. 前者若是初始化不传入初始容量,则使用最大int值,若是出队速度跟不上入队速度,会致使队列特别长,占用大量内存;
相关文章
相关标签/搜索