阻塞队列BlockingQueue(ArrayBlockingQueue)用法详解

目录

1.简介 

2.核心方法

2.1 ArrayBlockingQueue 阻塞式获取和新增元素的方法为:

2.2 ArrayBlockingQueue 非阻塞式获取和新增元素的方法为:

3.阻塞队列的实现类

4.总结


1.简介 

  BlockingQueue是基于阻塞机制实现的线程安全的队列。解决多线程之间数据共享的问题。阻塞机制的实现是通过在入队和出队时加锁的方式避免并发操作。常用于生产者-消费者模型中,往队列里添加元素的是生产者,从队列中获取元素的是消费者;通常情况下生产者和消费者都是由多个线程组成;生产者和消费者之间通过队列平衡两者的的处理能力、进行解耦等。

从下图我们可以很清楚看到,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出。

常用的队列主要有以下两种:(当然通过不同的实现方式,还可以延伸出很多不同类型的队列,DelayQueue就是其中的一种)

先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。

后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件

2.核心方法

2.1 ArrayBlockingQueue 阻塞式获取和新增元素的方法为:

  • put(E e):将元素插入队列中,如果队列已满,则该方法会一直阻塞,直到队列有空间可用或者线程被中断。
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        // 加锁
        lock.lockInterruptibly();
        try {
            while (count == items.length) {
              // 如果队列已满,线程阻塞,并添加到notFull条件队列中等待唤醒
              notFull.await();
            }
            // 如果队列未满,则调用enqueue方法进行入队操作
            enqueue(e);
        } finally {
            // 释放锁
            lock.unlock();
        }
    }


private void enqueue(E x) {
       //获取队列底层的数组
        final Object[] items = this.items;
        //将putindex位置的值设置为我们传入的x
        items[putIndex] = x;
        //更新putindex,如果putindex等于数组长度,则更新为0
        if (++putIndex == items.length)
            putIndex = 0;
        //队列长度+1
        count++;
        //通知队列非空,那些因为获取元素而阻塞的线程可以继续工作了
        notEmpty.signal();
    }

put方法进行阻塞式入队的基本流程为:

  1. 首先,在进行入队操作前,使用ReentrantLock进行加锁操作,保证只有一个线程执行入队或出队操作;如果锁被其他线程占用,则等待;

  2. 如果加锁成功,则首先判断队列是否满,也就是while(count == items.length);如果队列已满,则调用notFull.await(),将当前线程阻塞,并添加到notFull条件队列中等待唤醒;如果队列不满,则直接调用enqueue方法,进行元素插入;

  3. 当前线程添加到notFull条件队列中后,只有当其他线程有出队操作时,会调用notFull.signal()方法唤醒等待的线程;当前线程被唤醒后,还需要再次进行一次队列是否满的判断,如果此时队列不满才可以进行enqueue操作,否则仍然需要再次阻塞等待,这也就是为什么在判断队列是否满时使用while的原因,即避免当前线程被意外唤醒,或者唤醒后被其他线程抢先完成入队操作。

  4. 最后,当完成入队操作后,在finally代码块中进行锁释放lock.unlock,完成put入队操作 

  • take() :获取并移除队列头部的元素,如果队列为空,则该方法会一直阻塞,直到队列非空或者线程被中断。
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        // 加锁
        lock.lockInterruptibly();
        try {
            while (count == 0)
                // 判断队列是否为空,如果为空则线程阻塞,添加到notEmpty条件队列等待
                notEmpty.await();
            // 队列不为空,进行出队操作
            return dequeue();
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

private E dequeue() {
      	//获取阻塞队列底层的数组
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        //从队列中获取takeIndex位置的元素
        E x = (E) items[takeIndex];
        //将takeIndex置空
        items[takeIndex] = null;
        //takeIndex向后挪动,如果等于数组长度则更新为0
        if (++takeIndex == items.length)
            takeIndex = 0;
        //队列长度减1
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        //通知那些被打断的线程当前队列状态非满,可以继续存放元素
        notFull.signal();
        return x;
    }

  take方法与put方法类似,主要流程也是先加锁,然后循环判断队列是否为空,如果为空则添加到notEmpty条件队列等待,如果不为空则进行出队操作;最后进行锁释放

     当消费者从队列中 take 或者 poll 等操作取出一个元素之后,就会通知队列非满,此时那些等待非满的生产者就会被唤醒等待获取 CPU 时间片进行入队操作。

    当生产者将元素存到队列中后,就会触发通知队列非空,此时消费者就会被唤醒等待 CPU 时间片尝试获取元素。如此往复,两个条件对象就构成一个环路,控制着多线程之间的存和取。

方法实现

ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
  
  @RequestMapping(value = "put", method = RequestMethod.POST)
  public void putDate() throws InterruptedException {
    CompletableFuture.runAsync(() -> {
      for(int i = 1;i <=10;i++){
        // 向队列中添加元素,如果队列已满则阻塞等待
        try {
          queue.put(i);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println("生产者添加元素--:" + i);
      }
    }).exceptionally(e -> {
      System.out.println("Exception");
      return null;
    });

  }

  @RequestMapping(value = "take", method = RequestMethod.POST)
  public void takeDate() {
    try {
      int count = 0;
      while (true) {

        // 从队列中取出元素,如果队列为空则阻塞等待
        Integer element = queue.take();
        System.out.println("消费者取出元素:" + element);
        ++count;
        if (count == 10) {
          break;
        }
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    }

  }

生产者添加元素--:1
生产者添加元素--:2
生产者添加元素--:3
生产者添加元素--:4
生产者添加元素--:5
生产者添加元素--:6
消费者取出元素:1
消费者取出元素:2
消费者取出元素:3
消费者取出元素:4
消费者取出元素:5
消费者取出元素:6
生产者添加元素--:7
消费者取出元素:7
生产者添加元素--:8
生产者添加元素--:9
生产者添加元素--:10
消费者取出元素:8
消费者取出元素:9
消费者取出元素:10

2.2 ArrayBlockingQueue 非阻塞式获取和新增元素的方法为:

  • offer(E e):将元素插入队列尾部。如果队列已满,则该方法会直接返回 false,不会等待并阻塞线程。
  • poll():获取并移除队列头部的元素,如果队列为空,则该方法会直接返回 null,不会等待并阻塞线程。
  • add(E e):将元素插入队列尾部。如果队列已满则会抛出 IllegalStateException 异常,底层基于 offer(E e) 方法。
  • remove():移除队列头部的元素,如果队列为空则会抛出 NoSuchElementException 异常,底层基于 poll()
  • peek():获取但不移除队列头部的元素,如果队列为空,则该方法会直接返回 null,不会等待并阻塞线程。

非阻塞新增 offer()源码

public boolean offer(E e) {
        //确保插入的元素不为null
        checkNotNull(e);
        //获取锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
           	//队列已满直接返回false
            if (count == items.length)
                return false;
            else {
                //反之将元素入队并直接返回true
                enqueue(e);
                return true;
            }
        } finally {
            //释放锁
            lock.unlock();
        }
    }

获取元素poll() 源码

public E poll() {
        final ReentrantLock lock = this.lock;
        //上锁
        lock.lock();
        try {
        	//如果队列为空直接返回null,反之出队返回元素值
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }

新增方法详解

方法

队列满时处理方式

返回值

put(E e)

线程阻塞,直到中断或被唤醒

void

offer(E e)

直接返回 false

boolean

offer(E e, long timeout, TimeUnit unit)

指定超时时间内阻塞,超过规定时间还未添加成功则返回 false

boolean

add(E e)

直接抛出 IllegalStateException

异常

boolean

获取/移除元素:

方法

队列空时处理方式

方法返回值

take()

线程阻塞,直到中断或被唤醒

E

poll()

返回 null

E

poll(long timeout, TimeUnit unit)

指定超时时间内阻塞,超过规定时间还是空的则返回 null

E

peek()

返回 null

E

remove()

直接抛出 NoSuchElementException

异常

boolean

 contains(Object o) 来判断指定元素是否存在于队列中。

3.阻塞队列的实现类

实现类功能
ArrayBlockingQueue JDK1.5基于数组的阻塞队列,使用数组存储数据,并需要指定其长度,所以是一个有界队列
LinkedBlockingQueue JDK1.5基于链表的阻塞队列,使用链表存储数据,默认是一个无界队列;也可以通过构造方法中的capacity设置最大元素数量,所以也可以作为有界队列
SynchronousQueue JDK1.6一种没有缓冲的队列,生产者产生的数据直接会被消费者获取并且立刻消费
PriorityBlockingQueue基于优先级别的阻塞队列,底层基于数组实现,是一个无界队列
DelayQueue JDK1.8延迟队列,其中的元素只有到了其指定的延迟时间,才能够从队列中出队

4.总结

  1. ArrayBlockingQueue 的容量有限,一旦创建,容量不能改变。常用于多线程之间的数据共享,底层采用数组实现,从其名字就能看出来了。

  2. 保证线程安全,ArrayBlockingQueue 的并发控制采用可重入锁 ReentrantLock ,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。并且,它还支持公平和非公平两种方式的锁访问机制,默认是非公平锁。

  3. 线程间的等待唤醒实现:

  • 当队列已满时,生产者线程会调用 notFull.await() 方法让生产者进行等待,等待队列非满时插入(非满条件)。
  • 当队列为空时,消费者线程会调用 notEmpty.await()方法让消费者进行等待,等待队列非空时消费(非空条件)。
  • 当有新的元素被添加时,生产者线程会调用 notEmpty.signal()方法唤醒正在等待消费的消费者线程。
  • 当队列中有元素被取出时,消费者线程会调用 notFull.signal()方法唤醒正在等待插入元素的生产者线程。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值