BlockingQueue Interface In Java学习

一.从“生产者”和”消费者“模型谈起

生产者消费者问题,也称有限缓冲问题(Bounded-buffer problem),是一个多进程同步问题的经典案例。通过队列能够容易的实现多线程环境下的数据共享.生产者线程把准备好的数据从队尾插入,消费者线程从队头消费数据,以此解决其数据共享.但这是「柏拉图的理想国」,现实不尽是如此.有个前提是,队列长度是有限的.对于若干个生产者和消费者线程,其数据处理速率是不一致的,当生产者生产数据的速率大于消费者消费数据的速率,且经过一段时间,数据积累到快要挤满队列长度时,生产者线程就需要挂起(阻塞),等待消费者处理积累的数据,并由其唤醒;同样,当消费者消费数据速率大于生产者时,两个线程间就需要做相反的操作.
以上的操作,在Doug Lea写的concurrent包发布之前是需要程序员自己去实现的,而他带来了BlockingQueue,一种兼顾了线程安全和效率的接口,最大程度的保留了广大程序员的发量.因为我们站在了巨人的肩膀上,不用再去关心什么时候挂起线程,什么时候唤醒线程.

二.BlockingQueue Interface In Java简介

这是一个图:
BlockingQueue
Java提供了很几种BlockingQueue的实现,例如LinkedBlockingQueue, ArrayBlockingQueue, PriorityBlockingQueue, SynchronousQueue等等.其所有方法本质上都是原子的,并使用可重入锁或其他形式的并发控制.(这里膜一下这个留着德王威廉二世的胡子,脸上挂着腼腆笑容的Doug Lea大佬)
Doug Lea

根据其实例化方法不同,分为两种类型的BlokingQueue.

  • Unbounded Queue(无限队列):阻塞队列的容量将默认设置为Integer.MAX_VALUE.队列将名不副实,它将永远不会阻塞.因为它可能会随着你元素的添加,而增长到很大的规模. 创建方法:
BlockingQueue unboundedQueue = new LinkedBlockingDeque();
  • Bounded Queue(有限队列):通过在队列构造函数中传递队列的容量来创建它:
BlockingQueue boundedQueue = new LinkedBlockingDeque(7);

三.读一读BlockingQueue,ArrayBlockingQueue源码

1. BlockingQueue
BlockingQueue继承自Queue接口.包含三个添加元素的方法(add,offer,put)和三个删除元素的方法(take,poll,remove).下面分别介绍以上六个方法.
三种添加元素的方法:

  • add:添加指定元素到队列中,如果添加成功则返回true.如果元素由于队列大小限制无法添加,则会抛出IllegalStateException.
  • offer:添加指定元素到队列中,如果添加成功返回true,添加失败则返回false.
  • put:添加指定元素到队列中,如果队列满了,会阻塞到其不满为止.

三种删除元素的方法

  • take:检索并删除队头元素,如果队列为空,会阻塞到有元素删除为止.
  • poll:检索并删除队头元素,如果队列为空,会在规定的等待时间内阻塞直到有元素删除为止,若在阻塞时间内被中断,会抛出InterruptedException.
  • remove:基于对象找到队列中的元素,并删除.如果元素存在,删除,并返回true;如果指定元素的类和队列不兼容,会抛出ClassCastException.

值得一提的是,队列中不允许有空值,不然会抛空指针异常.

2. ArrayBlockingQueue
ArrayBlockingQueue属性
ArrayBlockingQueue继承自AbstractQueue,实现了BlockingQueue接口,包含了八个属性:

  • items:存储队列元素的数组.
  • takeIndex:取数据的索引,用于put,poll,peek或者remove方法.
  • putIndex:放数据的索引,用于put,offer或者add方法.
  • count:队列中元素的总数.
  • lock:可重入锁.
  • notEmpty:由lock创建的notEmpty条件对象.
  • notFull:由lock创建的notFull条件对象.
  • itrs:共享当前活跃的迭代器状态,如果没有则为空.运行队列操作更新迭代器状态.

接下来是关于ArrayBlockingQueue的入队(enqueue)和出队(dequeue)方法.

    private void enqueue(E e) {
    
    
        final Object[] items = this.items;
        items[putIndex] = e;
        if (++putIndex == items.length) putIndex = 0;
        count++;
        notEmpty.signal(); //入队完,notEmpty条件对象的siganl方法进行通知
    }
private E dequeue() {
    
    
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E e = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length) takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();  //出队完,调用notFull条件对象的signal方法进行通知        
        return e;
    }

ArrayBlockingQueue就是使用一个可重入锁和这个锁创建的两个条件对象进行并发控制.ArrayBlockingQueue直译过来就是数组阻塞队列,它特性就像数组一样,需要指定队列大小,并且指定之后不允许修改.

然后是关于ArrayBlockingQueue的三个添加元素的方法:add,offer,put.

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

add方法调用了offer方法.其实现如下:

public boolean offer(E e) {
    
    
        Objects.requireNonNull(e); //入队元素判空
        final ReentrantLock lock = this.lock;
        lock.lock(); //加可重入锁,保证调用offer方法的时候只有一个线程
        try {
    
    
            if (count == items.length) //数组判满
                return false;
            else {
    
    
                enqueue(e); //数组没满的情况下就入队
                return true;
            }
        } finally {
    
    
            lock.unlock(); //入队完了释放锁,让其他线程能够调用offer方法
        }
    }
  • put
public void put(E e) throws InterruptedException {
    
    
        Objects.requireNonNull(e); //入队元素判空
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly(); //加可重入锁,保证调用offer方法的时候只有一个线程
        try {
    
    
            while (count == items.length) //数组判满
                notFull.await(); //如果队列满了,阻塞当前线程并挂起,同时释放锁,调用notFull条件对象的await方法加入到其等待队列里.
            enqueue(e); //入队
        } finally {
    
    
            lock.unlock(); //释放锁
        }
    }

ArrayBlocking Queue提供的三个元素删除的方法:poll,take,remove.

  • poll
    public E poll() {
    
    
        final ReentrantLock lock = this.lock;
        lock.lock(); //加可重入锁,保证调用poll方法时只有一个线程
        try {
    
    
            return (count == 0) ? null : dequeue();
            //如果队列里没元素了,返回空;否则,调用dequeue方法出队.
        } finally {
    
    
            lock.unlock();
        }
    }
  • take
public E take() throws InterruptedException {
    
    
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly(); //加可重入锁,保证调用take方法的只有一个线程
        try {
    
    
            while (count == 0) 
                notEmpty.await(); //如果队列为空,阻塞当前线程并挂起,同时释放锁,调用notEmpty条件对象的await方法加入到其等待队列里.
                return dequeue(); //调用dequeue方法进行出队操作
        } finally {
    
    
            lock.unlock();
        }
    }
  • Remove
 public boolean remove(Object o) {
    
    
        if (o == null) return false;
        final ReentrantLock lock = this.lock;
        lock.lock(); // 加可重入锁,保证调用remove方法的只有一个线程
        try {
    
    
            if (count > 0) {
    
    
                final Object[] items = this.items;
                for (int i = takeIndex, end = putIndex,
                         to = (i < end) ? end : items.length;
                     ; i = 0, to = end) {
    
    
                    for (; i < to; i++)
                        if (o.equals(items[i])) {
    
     //待删除元素与数组中的元素一致
                            removeAt(i); //只有才持有锁的时候才能调用removeAt方法删除指定下标的元素	
                            return true;
                        }
                    if (to == end) break;
                }
            }
            return false;
        } finally {
    
    
            lock.unlock(); //释放锁,让其他线程能够调用remove方法
        }
    }
  • remove方法调用的removeAt方法
void removeAt(final int removeIndex) {
    
    
        final Object[] items = this.items;
        if (removeIndex == takeIndex) {
    
     //如果要删除的索引刚好是取索引的位置,直接删除取索引指向的元素,然后取索引+1
            items[takeIndex] = null;
            if (++takeIndex == items.length) takeIndex = 0;
            count--;
            if (itrs != null) 
                itrs.elementDequeued(); //更新迭代器状态
        } else {
    
     // 若元素不在取索引位置,则移动元素,更新取索引和放索引的位置
            for (int i = removeIndex, putIndex = this.putIndex;;) {
    
    
                int pred = i;
                if (++i == items.length) i = 0;
                if (i == putIndex) {
    
    
                    items[pred] = null;
                    this.putIndex = pred;
                    break;
                }
                items[pred] = items[i];
            }
            count--;
            if (itrs != null)
                itrs.removedAt(removeIndex); // 迭代器的方式删除在待删除索引位置的元素
        }
        notFull.signal(); // 调用notFull条件对象的signal方法进行通知   
    }

关于ArraryBlockingQueue的关于新增和删除元素的方法总结如下:

  1. 关于add方法:内部调用了offer方法.添加成功返回true,队列满了则抛出IllegalStateException.
  2. 关于offer方法:添加成功返回true,队列满了则返回false.
  3. 关于put方法:越王勾践式的添加方法.队列满了,会阻塞线程,直到有消费者消费了队列中的元素,才有可能被唤醒,进行元素添加.
  4. 关于poll方法:队列为空就返回null,否则返回队头元素.
  5. 关于take方法:越王勾践式的删除方法.队列为空,阻塞线程,直到有生产者往队列中生产了元素,才有被唤醒的可能,进行元素删除.
  6. 关于remove方法:调用了removeAt方法,删除元素成功则返回true,否则返回false.
  7. 以上的六个方法均使用了可重入锁,保证操作的原子性.

猜你喜欢

转载自blog.csdn.net/MR_Peach07/article/details/107349200
今日推荐