阻塞队列-ArrayBlockingQueue源码分析(看完这一篇就够了)

(一)基础部分

1、先看看阻塞队列的接口架构图
在这里插入图片描述
通过接口架构图可知,阻塞队列BlockingQueue的父类Queue(队列)和List集合与Set集合并列存在。而BlockingQueue有两个兄弟类,Deque(双管队列)、AbstractQueue(非阻塞队列)

2、什么是阻塞队列?

阻塞队列,BlockingQueue(接口),是在队列(Queue)的基础上支持了两个附加操作的队列。
2个附加操作:
支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程,直到队列不满。
支持阻塞的移除方法:队列空时,获取元素的线程会等待队列变为非空。

3、阻塞队列的应用场景

阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。简而言之,阻塞队列是生产者用来存放元素、消费者获取元素的容器。

4、阻塞的队列的分类(七种)

1)ArrayBlockingQueue 数组结构组成的有界阻塞队列。
此队列按照先进先出(FIFO)的原则对元素进行排序,但是默认情况下不保证线程公平的访问队列,即如果队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平(即先阻塞,先插入)的。
2)LinkedBlockingQueue一个由链表结构组成的有界阻塞队列
此队列按照先出先进的原则对元素进行排序
3)PriorityBlockingQueue 支持优先级的无界阻塞队列
4)DelayQueue 支持延时获取元素的无界阻塞队列,即可以指定多久才能从队列中获取当前元素
5)SynchronousQueue不存储元素的阻塞队列,每一个put必须等待一个take操作,否则不能继续添加元素。并且他支持公平访问队列。
6)LinkedTransferQueue由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,多了tryTransfer和transfer方法
transfer方法
如果当前有消费者正在等待接收元素(take或者待时间限制的poll方法),transfer可以把生产者传入的元素立刻传给消费者。如果没有消费者等待接收元素,则将元素放在队列的tail节点,并等到该元素被消费者消费了才返回。
tryTransfer方法
用来试探生产者传入的元素能否直接传给消费者。,如果没有消费者在等待,则返回false。和上述方法的区别是该方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。
7)LinkedBlockingDeque链表结构的双向阻塞队列,优势在于多线程入队时,减少一半的竞争。

5、什么叫做阻塞队列的有界和无界

阻塞队列有一个非常重要的属性,那就是容量的大小,分为有界和无界两种。
无界队列意味着里面可以容纳非常多的元素,例如 LinkedBlockingQueue 的上限是 Integer.MAX_VALUE,约为 2 的 31 次方,是非常大的一个数,可以近似认为是无限容量,因为我们几乎无法把这个容量装满。但是有的阻塞队列是有界的,例如 ArrayBlockingQueue 如果容量满了,也不会扩容,所以一旦满了就无法再往里放数据了。

6、公平锁

比如说现在队列是满的,还有很多线程执行 put 操作,必然会有很多线程阻塞等待,当有其它线程执行 take 时,会唤醒等待的线程,如果是公平锁,会按照阻塞等待的先后顺序,依次唤醒阻塞的线程,如果是非公平锁,会随机唤醒沉睡的线程

(二)阻塞队列的四种处理方式

在这里插入图片描述
1、抛出异常

  • 添加:add
  • 删除:remove
public class test04 {
    public static void main(String[] args) {
        BlockingQueue  blockingQueue=new ArrayBlockingQueue(3);//参数为队列的大小
        //add() --队列添加元素
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));

        //队列满了,在添加报错
        System.out.println(blockingQueue.add("d"));

        System.out.println("***********************");
         System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        //队列空,报异常
        System.out.println(blockingQueue.remove());

    }
}

队列满再次添加元素,会报异常,如下图:
在这里插入图片描述

为什么会报异常?

查看add的部分源码

public boolean add(E e) {
		//调用offer传入要添加的元素e
        if (offer(e))
        	//添加成功返回true
            return true;
        else
        	//抛出异常Queue full
            throw new IllegalStateException("Queue full");
    }

查看源码可知,add方法的底层调用了offer方法,添加元素成功返回true,失败抛出异常

2、不抛出异常,返回特殊值

  • 添加:offer
  • 移除:pol

l

public static void test02(){
        BlockingQueue  blockingQueue=new ArrayBlockingQueue(3);//参数为队列的大小
        //offer() --队列添加元素
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));

        //队列满了,再次添加为false,不会报异常
        System.out.println(blockingQueue.offer("d"));

        //poll()--移除队列中的元素
        System.out.println("***********************");
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        //队列空,再次移除为null
//        System.out.println(blockingQueue.poll());
    }

运行结果:添加元素,超过队列大小会返回一个false
在这里插入图片描述
运行结果:移除元素,当队列中元素为null时,再次移除会返回一个null

3、一直阻塞

  • 添加:put
  • 移除:take
public static void test03() throws InterruptedException {
    BlockingQueue  blockingQueue=new ArrayBlockingQueue(3);//参数为队列的大小
    //offer() --队列添加元素
    blockingQueue.put("a");
    blockingQueue.put("b");
    blockingQueue.put("c");
    blockingQueue.put("d");

    //队列满了,再次添加将进入等待状态
    System.out.println(blockingQueue.offer("d"));

    //poll()--移除队列中的元素
    System.out.println("***********************");
    System.out.println(blockingQueue.take());
    System.out.println(blockingQueue.take());
    System.out.println(blockingQueue.take());
    //队列空,再次移除将进入等待状态
    System.out.println(blockingQueue.take());
}

运行结果:添加元素,超过队列大小将进入等待状态,程序不停止,一直等待,直到队列中有元素被移除
在这里插入图片描述
思考:为什么会一直阻塞?

(看完下面的源码分析就会有结果了)

运行结果:移除元素,队列元素为空时,将进入等待状态,程序不停止,直到队列中有元素添加进来
在这里插入图片描述
4、超时退出

  • 添加操作:offer(e,time,unit),是offer(e)方法的一个构造方法,新增了参数time。意为当添加元素,队列满时,再次添加,将进入等待状态,等待时长为参数给定时长,如果超过给定时长,还没有元素移除队列中,就自动退出

  • 移除操作:poll(time.unit),是poll()方法的一个构造方法,新增参数time。当队列为空时,再次移除,将进入等待状态,等待时长为参数给定时长,超过时长将退出

(三)源码分析-ArrayBlockingQueue

1、put和take操作
队尾插入元素(put),队头拿出元素(take)

2、特点

  • 有界数组,容量创建,不可修改
  • 元素有序,先进先出
  • 队列满或空时,put和take会被阻塞

3、源码分析

初始化定义,数据结构部分

// 初始化数组
final Object[] items;

// 拿数据的索引位置
int takeIndex;

// 放数据的索引位置
int putIndex;

// 当前队列元素个数
int count;

// 可重入的锁
final ReentrantLock lock;

// 移除元素的队列
private final Condition notEmpty;

// 添加元素的队列
private final Condition notFull;

构造方法

有两个参数:队列大小,是否公平

public ArrayBlockingQueue(int capacity, boolean fair) {
		//队列大小小于等于0
        if (capacity <= 0)
        	//抛出异常
            throw new IllegalArgumentException();
         //new一个object类型的数组,数组大小为队列大小,并赋值给初始化数组
        this.items = new Object[capacity];
        //定义一个可重入锁,传入的参数是否公平
        lock = new ReentrantLock(fair);
        //队列非空,创建Condition,在执行put操作成功后使用
        notEmpty = lock.newCondition();
        //队列非满,创建Condition,在执行take操作成功后使用
        notFull =  lock.newCondition();
    }

在这里补充下Condition知识

Condition,为JDK1.5之后新出显得一个类,await(),signal()用来替换传统的Object的wait(),notify()操作,需要搭配Lock锁使用

创建方式

Lock lock=new ReentrantLock();
Condition condition1=lock.newCondition();

方法
在这里插入图片描述
通过查看源码,可知第二个参数fair,主要用于读写锁是否公平,在队列满时进行堵塞后,如果有元素移出队列,如果为公平锁,则按照阻塞的先后顺序入队,FIFO原则;如果为非公平锁,则随机释放阻塞线程,线程之间会有竞争

添加元素

public void put(E e) throws InterruptedException {
		//判断入队元素是否为null
        checkNotNull(e);
        //定义可重入锁
        final ReentrantLock lock = this.lock;
        //线程中断
        lock.lockInterruptibly();
        try {
        	//判断队列是否已满
            while (count == items.length)
            	//队列满进入无限等待状态,直到有元素出队
                notFull.await();
             //执行入队操作
            enqueue(e);
        } finally {
        	//释放锁
            lock.unlock();
        }
    }

checkNotNull方法源码

//传入非空判断
private static void checkNotNull(Object v) {
		//判断入队元素是否为null值
        if (v == null)
        	//抛出异常
            throw new NullPointerException();
    }

enqueue方法源码

 private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        //计算本次入队的索引位置
        items[putIndex] = x;
        //判断下一个入队元素索引是否为最后一个
        if (++putIndex == items.length)
        	//如果是队列最后一个,下次添加将从0开始
            putIndex = 0;
        //队列执行++操作
        count++;
        //通知阻塞线程
        notEmpty.signal();
    }

从添加的源码可知,添加有两种情况

1.添加的元素不在队尾,元素直接入队

在这里插入图片描述
2.添加元素索引在队尾,下次添加元素直接从队头索引为0的位置开始

 if (++putIndex == items.length)
            putIndex = 0;

在这里插入图片描述
拿数据

 public E take() throws InterruptedException {
 		//定义可重入锁
        final ReentrantLock lock = this.lock;
        //线程中断
        lock.lockInterruptibly();
        try {
        	//判断队列中元素个数是否为0
            while (count == 0)
            	//进入无限等待操作,直到队列中有元素入队
                notEmpty.await();
             //调用出队方法
            return dequeue();
        } finally {
        	//释放锁
            lock.unlock();
        }
    }

dequeue方法源码

private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        //x 为出队元素的索引位置
        E x = (E) items[takeIndex];
        //出队索引位置的数据置为null
        items[takeIndex] = null;
        //判断出队索引是否为最后一个
        if (++takeIndex == items.length)
        	//下次出队从队头索引为0的位置开始
            takeIndex = 0;
        //队列大小执行-1操作
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();
        return x;
    }

从源码可知,每次拿出数据,队列的takeIndex值+1,如果takeIndex索引位于队列末尾位置时,下次take操作从队头索引的位置为0处开始。代码

if (++takeIndex == items.length)
            takeIndex = 0;

删除元素

 void removeAt(final int removeIndex) {
        // assert lock.getHoldCount() == 1;
        // assert items[removeIndex] != null;
        // assert removeIndex >= 0 && removeIndex < items.length;
        final Object[] items = this.items;
        //情况1:移除元素索引等于拿元素索引
        if (removeIndex == takeIndex) {
            // removing front item; just advance
            //将takeIndex处索引元素置为null,即删除元素
            items[takeIndex] = null;
            //判断takeIndex索引是否为队尾
            if (++takeIndex == items.length)
            	//在队尾,下次拿元素从0开始
                takeIndex = 0;
            //队列大小执行-1操作
            count--;
            if (itrs != null)
                itrs.elementDequeued();
        } else {
            // an "interior" remove
            // slide over all others up through putIndex.
            final int putIndex = this.putIndex;
            //死循环
            for (int i = removeIndex;;) {
            	//删除元素的下一个位置为next
                int next = i + 1;
                //判断next索引是否位于队尾
                if (next == items.length)
                	//位于队尾下次从0开始
                    next = 0;
                 //情况2:下一个元素不等于putIndex
                if (next != putIndex) {
                	//下一个元素往前移动
                    items[i] = items[next];
                    i = next;
                  //情况3:下一个元素等于putIndex
                } else {
                	//删除该索引位置的元素
                    items[i] = null;
                    //下次执行put操作的索引位置为删除元素的索引位置
                    this.putIndex = i;
                    break;
                }
            }
            //队列大小-1
            count--;
            if (itrs != null)
                itrs.removedAt(removeIndex);
        }
        //唤醒阻塞线程
        notFull.signal();
    }

通过源码可知,删除元素新增删除索引:removeIndex,一共有两种情况:

第一种:removeIndex == takeIndex
在这里插入图片描述
第二种情况又分为两种情况

1.removeIndex+1!=putIndex,下一个元素往前移动一位
在这里插入图片描述
2.如果 removeIndex + 1 == putIndex 的话,就把 putIndex 的值修改成删除的位置
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值