JUC--ArrayBlockingQueue源码分析 (基于JDK1.8)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ONROAD0612/article/details/82871432

1 概述

这是一个依赖于数组实现的有界的阻塞队列,采用先进先出的规则,队列的头元素是入队最久的元素,队列的尾元素是入队时间最晚的元素。队列的大小打队列初始化的时候一旦确定了就没法进行改变,当尝试向满队列中插入数据和从空队列中获取数据都将产生阻塞。另一方面这个队列也可以保证等待线程的公平性,只需要再初始化的时候设置公平性即可,当然,默认情况下是非公平的。

2 使用示例

ArrayBlockingQueue比较典型的使用就是用于实现生产者和消费者的模式,并且这里的队列大小的固定的。下面我们来看一下使用的示例:

import java.util.concurrent.ArrayBlockingQueue;

/**
 * 演示ArrayBlockingQueue的使用
 * @author: LIUTAO
 * @Date: Created in 2018/9/27  19:36
 * @Modified By:
 */
public class ArrayBlockingQueueDemo {
    static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(5);

    public static void main(String[] args) {

        //producer线程
        for (int i = 0; i < 10 ; i++){
            new Thread(() -> {
                try {
                    arrayBlockingQueue.put(Thread.currentThread().getName());
                    System.out.println(Thread.currentThread().getName() + "have put");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }

        //consumer线程
        for (int i = 0; i < 10 ; i++){
            new Thread(() -> {
                try {
                    System.out.println(arrayBlockingQueue.take() + "have got");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行代码,我们可以发现生产者线程一开始就有五个线程执行完成,将数据加入了队列,而其余线程都是当消费者执行一下,然后生产者再执行。这是因为对列的容量为5,当队列被填满的时候生产生线程就只有等待。

3 类的继承关系

这里我们来看一下类的继承关系。

从这里我们可以看出ArrayBlockingQueue拥有阻塞队列的特性。

4 属性

    //存放队列的数据
    final Object[] items;

    //即将被消费的数组数据索引
    int takeIndex;

    //即将存入的数组数据的索引
    int putIndex;

    //队列中的元素个数
    int count;

    //保证线程安全的重入锁
    final ReentrantLock lock;

    //不为空的条件
    private final Condition notEmpty;

    //有空闲位置的条件
    private final Condition notFull;

从上面我们可以看出,为了保证线程的安全性和线程间的通信,这里使用了ReentrantLock和Condition。

5 构造函数

针对构造函数,这里我们仅仅查看一个比较核心的就行。

public ArrayBlockingQueue(int capacity, boolean fair) {

        //验证参数的合法性
        if (capacity <= 0)
            throw new IllegalArgumentException();
        //初始化存放数据的数组
        this.items = new Object[capacity];
        
        //初始化保证线程安全的重入锁
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

从上面我们可以看出构造函数其实就只做了两件事:(1)初始化数据存放数据。(2)初始化保证线程安全的重入锁。

6 核心函数

核心函数我们通过查看“JUC--队列概述” 可以知道针对阻塞队列入队和出队一共有四组函数,这里我们仅仅分析put和take函数,其余的函数逻辑大同小异。

6.1 put函数

首先直接上put函数的源码:

public void put(E e) throws InterruptedException {

        //检查e是否为空,为空则直接抛出NullPointerException
        checkNotNull(e);
        final ReentrantLock lock = this.lock;

        //加锁
        lock.lockInterruptibly();
        try {

            //当当前队列中的元素个数和数组大小相等的时候等待
            while (count == items.length)
                notFull.await();

            //数据入队
            enqueue(e);
        } finally {

            //释放锁
            lock.unlock();
        }
    }

上面的逻辑比较清晰简单,我们就直接来看一下入队是怎么操作的。

private void enqueue(E x) {
        
        //获得数据存储数组
        final Object[] items = this.items;

        //存入数据
        items[putIndex] = x;

        //当数据存入了数组的最后一个位置,为了保证FIFO规则,下一次从数组第一个位置开始存储
        if (++putIndex == items.length)
            putIndex = 0;
        count++;

        //唤醒处于等待状态的消费线程
        notEmpty.signal();
    }

上面的逻辑也比较简单,可谓充分使用了Condition,接下来我们来看一下take的实现。

6.2 take函数

同样,直接上源码:

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;

        //加锁,注意:这是响应中断的
        lock.lockInterruptibly();
        try {
           
            //当当前队列没有数据的时候等待
            while (count == 0)
                notEmpty.await();

            //出对
            return dequeue();
        } finally {

            //释放锁
            lock.unlock();
        }
    }

可以看出这里还是当没有数据的时候进行等待,知道put添加数据并调用notEmpty.signal()。

那么这里出对又是咋个实现的呢?

 private E dequeue() {
        
        //获取数组
        final Object[] items = this.items;
        
        //获取数据
        E x = (E) items[takeIndex];
        items[takeIndex] = null;

        //当当前获取数据的索引为数组的最后一个索引的时候,下一次从数组的首位开始获取
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();

        //通知生产者线程
        notFull.signal();
        return x;
    }

总地说来,也比较简单,这里就不过多分析了。

7 总结

经过上面的分心与学习,我们了解到,其实ArrayBlockingQueue就是使用数组存储数据,然后使用ReentrantLock来保证线程的安全性,使用Condition来进行线程间的通信。接下来博主会继续分析其他的队列类的实现,欢迎关注。

猜你喜欢

转载自blog.csdn.net/ONROAD0612/article/details/82871432