【JAVA多线程】LinkedBlockingQueue 源码分析

在这里插入图片描述

LinkedBlockingQueue

基于链接节点的可选有界阻塞队列。此队列对元素进行 FIFO(先进先出)排序。队列的头部是在队列中时间最长的元素。队列的尾部是在队列中时间最短的元素。新元素被插入到队列的尾部,队列检索操作获取队列头部的元素。链接队列通常比基于数组的队列具有更高的吞吐量,但在大多数并发应用程序中性能更不可预测。

可选的容量绑定构造函数参数用作防止过度队列扩展的一种方式。容量,如果未指定,则等于Integer.MAX_VALUE。链接节点在每次插入时动态创建,除非这会使队列超出容量。

用法

LinkedBlockingQueue可以是有界队列,也可以是无界队列。数组的扩容是一件很麻烦的事情,因此ArrayBlockingQueue必须指定容量,数组一经创建就不可变了,它只能做有界队列。而LinkedBlockingQueue由于使用的是链表结构,元素的增删很容易实现,不需要初始化时就开辟内存,更适合做无界队列。默认的构造函数中,LinkedBlockingQueue的容量为Integer.MAX_VALUE,可看作是无界队列。

import java.util.concurrent.*;

public class Test {


    public static void main(String[] args) {
        LinkedBlockingQueue<String> mQueue = new LinkedBlockingQueue<>();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        String s = mQueue.take();
                        System.out.println("取出数据:" + String.valueOf(s));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                int count = 0;
                while (true) {
                    System.out.println("装载数据:" + count);
                    try {
                        mQueue.put(String.valueOf(count));
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    count++;
                }
            }
        }).start();

    }
}

成员变量

// 容量,默认为Integer.MAX_VALUE
private final int capacity;
// 元素的个数
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();

ArrayBlockingQueue使用int变量记录数量,而LinkedBlockingQueue使用AtomicInteger原子类记录数量。这是因为ArrayBlockingQueue生产消费使用同一把锁,任一时刻最多只有一个线程可以修改count。而LinkedBlockingQueue生产和消费使用的是不同的锁,两者线程都可以修改count,存在线程安全问题,因此使用原子类来保证数据安全。

head和last分别指向链表的首尾节点,很好理解。takeLock和putLock分别是消费者和生产者竞争的锁,每把锁各自有一个等待队列Condition,当队列满时,线程会被放入notFull等待,队列空时,线程会被放入notEmpty等待。

构造函数

默认容量为Integer.MAX_VALUE,可看作是一个无界队列。使用无界队列是比较危险的,当生产速度远高于消费速度时,会导致大量数据堆积,JVM内存溢出。

public LinkedBlockingQueue(int capacity) {
	if (capacity <= 0) throw new IllegalArgumentException();
	this.capacity = capacity;	
	last = head = new Node<E>(null);// 头尾节点指向一个空的Node节点
}

默认情况下,头尾指针会指向一个空的Node节点,因此,链表中至少会有一个节点。

生产元素

put在此队列的尾部插入指定元素,如有必要,等待空间可用。

 public void put(E e) throws InterruptedException {
        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;
        putLock.lockInterruptibly();// 以响应中断的方式竞争锁
        try {
           
            while (count.get() == capacity) {
                notFull.await();// 队列满,阻塞等待其他线程消费
            }
            enqueue(node);// 入队
            c = count.getAndIncrement();//先读取再递增的
            if (c + 1 < capacity)// 只要队列没满,就通知其他线程继续生产
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)//0代表队列有一个元素了。
            signalNotEmpty();//有一个元素时开始通知消费线程消费数据
    }
private void enqueue(Node<E> node) {
    // 尾节点指向新node,前任尾节点的next指向新node
    last = last.next = node;
}

它和offer方法的区别就是,当队列已满,它不会直接返回false,而是调用notFull.await无限期的阻塞,直到被唤醒。

消费元素

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) {// 队列空,阻塞等待生产
                notEmpty.await();
            }
            x = dequeue();// 队头元素出队
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();// 队列中还有元素,继续通知其他消费线程
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)// 队列中还有 capacity-1 个元素
            signalNotFull();// 通知生产者线程,只通知一次,生产者会自行通知
        return x;
    }
    
private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;// head指向head.next
        E x = first.item;
        first.item = null;
        return x;
    }

它和poll方法的区别就是,当队列中没有元素时,不是直接返回null,而是调用notEmpty.await()将当前线程挂起并放入消费者等待队列中,当队列中有元素时,它将被唤醒。

总结

LinkedBlockingQueue是使用单向链表结构实现的线程安全的阻塞队列,它可以是有界队列,也可以是无界队列。和ArrayBlockingQueue不同的是,LinkedBlockingQueue生产者和消费者各自竞争的是不同的锁,所以队列的生产和消费可以同时进行的,所以理论上,LinkedBlockingQueue的并发性能会稍好一些。

两把锁,对应的会有两个等待队列Condition。队列空时,消费者线程会被挂起并放入notEmpty。队列满时,生产者线程会被挂起并放入notFull。

当使用无界队列时,要格外小心消息堆积导致的内存溢出。

猜你喜欢

转载自blog.csdn.net/qq_15604349/article/details/124498661