队列:LinkedBlockingQueue源码解析


队列是很重要的API,线程池、读写锁、消息队列等技术和框架的底层原理都是队列,是很多高级API的基础。

LinkedBlockingQueue可以理解为一个典型的生产者-消费者模型。

1.整体架构

LinkedBlockingQueue,链表阻塞队列,底层的数据结构时链表,且队列是可阻塞的。
image

Queue接口和BlockingQueue接口

从类关系图中可以看出两条线路,

线路1
AbstractQueue --> AbstractCollection --> Collection --> Iterable

这条路线主要是想复用Collection和迭代器的一些操作。

线路2
BlockingQueue --> Queue -->Collection

Queue是最基础的接口,几乎所有队列实现类都实现这个接口,该接口定义了队列的三类基本操作,

  • 新增操作
    add方法队列满时抛出异常
    offer方法队列满时返回false
  • 查看并删除操作
    remove方法队列为空时返回false
    poll方法队列为空时返回null
  • 只查看但不删除操作
    element方法队列为空时抛出异常
    peek方法队列为空时返回null

BlockingQueue在Queue的基础上添加了阻塞的概念,如一直阻塞或阻塞指定时间,

操作类型抛异常特殊值阻塞阻塞一段时间
新增直至队列已满addoffer返回falseputoffer设置超时,超时返回false
弹出队列头至队列为空remove返回false;poll返回nulltakepoll设置超时,超时返回null
查看队列头但不弹出elementpeek返回null

类注释

  1. 基于链表的阻塞队列,底层数据结构是链表
  2. 链表维护先入先出队列,新元素添加到队尾,获取元素时从头部取出
  3. 链表的大小在初始化时候可以设置,默认是Integer的最大值
  4. 可以使用Collection和Iterable的所有操作

内部结构

LinkedBlockingQueue可以分为三个部分,存储链表+锁+迭代器

// 链表节点
static class Node<E> {
    E item;

    Node<E> next;

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

// 链表容量,默认情况下是 Integer.MAX_VALUE
private final int capacity;

// 使用原子性的Integer类对象记录链表元素数目
private final AtomicInteger count = new AtomicInteger();

// 链表头
transient Node<E> head;

// 链表尾
private transient Node<E> last;

// 获取元素时候的锁,take和poll方法会用到
private final ReentrantLock takeLock = new ReentrantLock();

// 获取元素时的条件队列,可以理解为 等待链表not empty时才获取元素
private final Condition notEmpty = takeLock.newCondition();

// 添加元素时候的锁,put和offer方法会用到
private final ReentrantLock putLock = new ReentrantLock();

// 添加元素时的条件队列,可以理解为 等待链表not Full时才添加元素
private final Condition notFull = putLock.newCondition();

// 内部实现的迭代器
private class Itr implements Iterator<E> {
	......
}

三种结构各司其职,

  1. 链表作用是保存当前节点,节点使用了泛型,所以节点中的数据可以是任意的对象。
  2. 锁有take和put锁,目的是保证队列操作时线程安全,同时take和put操作可以同时进行,互不影响。

初始化

LinkedBlockingQueue有三种初始化方式,

// 无参数初始化,默认容量是Integer.MAX_VALUE
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);	// 注意,head节点一定是固定的值为null的节点对象,保证了不会空指针异常
}

// 使用集合对象初始化
public LinkedBlockingQueue(Collection<? extends E> c) {
    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();	// 集合类元素不能为null
            if (n == capacity)
                throw new IllegalStateException("Queue full");
            enqueue(new Node<E>(e));
            ++n;
        }
        count.set(n);
    } finally {
        putLock.unlock();
    }
}

初始化源码中包含的信息,

  1. 初始化时容量大小不影响性能,只影响以后的使用,初始化队列太小会导致过早报出IllegalStateException异常
  2. 源码中for循环的形式并不优雅,添加一个元素后检查是否超过capacity是一种低效的方式。完全可以先得到集合对象的size,直接判断是否与设定的capacity冲突。

2.源码解析

入队和出队操作

队列是先进先出的结构,入队元素会被添加到队尾,出队元素是队列头部元素。分别对应于enqueuedequeue方法。

1) enqueue方法

入队方法很简单,在队尾添加节点后将last指针指向新加入的节点对象。

private void enqueue(Node<E> node) {
    last = last.next = node;
}

初始化方法中,last指针首先会知道一个item为null的节点对象上,因此在添加节点的过程中不会出现空指针异常。

2) dequeue方法

出队每次需要将队列头部元素取出,注意需要保证head指针始终指向的是item为null的节点。

private E dequeue() {
    Node<E> h = head;
    Node<E> first = h.next;	// 此时h是初始化时创建的item为null的节点,first指向的是实际的队列头部
    h.next = h; // help GC
    head = first;	// head指针指向队列头部节点
    E x = first.item;
    first.item = null;	// 返回头部节点的item值,并将头部节点的item设置为null,成为新的head节点
    return x;
}

新增节点操作

1) add方法

add方法在容量达到capacity时会抛出异常,

public boolean add(E e) {
    if (offer(e))
        return true;
    else
        throw new IllegalStateException("Queue full");
}

可见该方法底层调用的是offer方法,通过offer方法的返回值判断是否添加成功,如果添加失败则会抛出异常。

2) offer方法

offer方法不会抛出异常,而是在添加成功后返回true,反之,返回false。

public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    final AtomicInteger count = this.count;
    if (count.get() == capacity)	// 队列满了返回false
        return false;
        
    int c = -1;		// 注意这里 c初始化值为是-1
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;	// 争取到put锁后,上锁
    putLock.lock();
    try {
        if (count.get() < capacity) {
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();	// 如果队列未满,添加元素后count值值赋予c变量,之后增加
        }
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();	// 如果c从-1到0说明是第一次加入元素,队列从空变为非空,唤醒put等待队列中的线程
    return c >= 0;
}

private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}

3) put方法

public void put(E e) throws InterruptedException {
    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);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        // 如果队列已满则阻塞,可简单记忆为 wait not full,等待未满的过程
        while (count.get() == capacity) {
            notFull.await();
        }
        enqueue(node);
        c = count.getAndIncrement();
        if (c + 1 < capacity)
            notFull.signal();	// 添加后如果还未满,可尝试唤醒另一个put等待队列中的对象
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
}

三种添加元素的方法进行如下总结,

  1. 添加元素的第一步是上锁,保证线程安全
  2. 新增数据直接添加到队尾即可
  3. 新增数据时,如果容量满了,则当前线程阻塞,直至队头元素被取出使得队列中存在空位。阻塞是通过锁实现的,具体原理也是等待队列,会在以后锁相关的笔记中说明
  4. 添加元素成功后,如果队列未满,会尝试唤醒putLock的等待线程;同时,如果队列不为空,会唤醒takeLock的等待线程。保证一旦满足put或take的条件,就能够唤起等待线程,不会浪费时间。

offer方法可以设置阻塞一定时间,具体原理与put方法相同,只是在一定时间范围内阻塞,
image

删除节点操作

队列的删除节点操作返回的是队列头节点的值,但是具体返回形式有两种,一种是返回值的同时删除头节点,另一种是返回值但是不删除节点。

删除数据关注两点,

  1. 删除原理
  2. 查看并删除和查看不删除在实现方式上的区别

查看并删除

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 {
        while (count.get() == 0) {	// 如果队列为空,则阻塞,wait not empty
            notEmpty.await();
        }
        x = dequeue();
        c = count.getAndDecrement();
        if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}
  1. 先上锁
  2. 如果队列为空,则阻塞,直至队列非空;反之,使用dequeue方法删除队头节点,并返回队头节点的值
  3. 如果满足put或take的条件,会尝试唤醒等待队列中的线程
2) poll方法(非阻塞)
public E poll() {
    final AtomicInteger count = this.count;
    if (count.get() == 0)	// 如果队列为空直接返回null
        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;
}

与take方法相比,不存在阻塞过程。

查看但不删除—peek方法

public E peek() {
    if (count.get() == 0)	// 队列为空则返回null
        return null;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        Node<E> first = head.next;
        if (first == null)
            return null;
        else
            return first.item;	// 获取队头节点的item值并返回
    } finally {
        takeLock.unlock();
    }
}

该过程不涉及使用dequeue删除对头节点。

总结

LinkedBlockingQueue可以应用到多线程环境中,如消费者-生产者模型。队列本身也是很重要的数据结构。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值