Java多线程(三) 阻塞队列

1.阻塞队列介绍

队列是一种非常重要的数据结构,而阻塞队列是队列的一种,它最大的特点是所有的操作都是线程安全的。日常开发中常被用来处理多线程间的数据传递。Excutor框架提供的各种线程池,都是在阻塞队列的基础上实现的。在Java的并发类库中提供了各式各样的线程安全的队列实现,它们的结构如下图所示:

Java多线程(三) 阻塞队列_第1张图片

从行为特征来看,大部分的队列都是实现了BlockingQueue接口。在常规操作的基础上Blocking意味着其提供了特定的等待操作:获取时(take)等待元素入队,或者插入时(put)等待队列出现空位。

2.BlockingQueue的核心方法:

  • offer(E e):添加一个元素到队列中,如果队列能够容纳则返回true,否则返回false。此方法不会阻塞当前线程。此方法还有一个重载方法,可以设置等待时间,如果在指定时间内不能将元素放入队列则返回失败。
  • put(E e):将一个元素添加到队列中,如果队列已满,则当前线程会被阻塞,直到队列中有空位。
  • take():取走排在队列首位的元素,如果队列为空则阻塞当前线程进入等待状态,直到队列有新元素加入。
  • poll(time):取走排在队列首位的元素,如果不能立即取出,则等待指定的时间,取不到时返回null。
  • drainTo():一次性从队列中取出所有可用的元素。通过此方法可以提高获取数据的效率,无需多次分批加载。

3.Java中常用的阻塞队列

在Java的并发框架中提供了7中阻塞队列,如下所示:

3.1 ArrayBlockingQueue

ArrayBlockingQueue是用数组实现的有界阻塞队列,并按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证线程公平的访问队列。公平访问队列就是指阻塞的所有线程,当队列可用时按照阻塞的先后顺序访问队列。即先阻塞的线程优先访问队列。通常情况下为了保证公平性会降低吞吐量。创建公平队列的方式如下所示:

ArrayBlockingQueue queue=new ArrayBlockingQueue(100,true)
3.2 LinkedBlockingQueue

它是一个基于链表的阻塞队列,此队列按照先进先出的原则对元素进行排序,内部的数据存储是基于链表数据结构来实现的。之所以能高效的实现并发是因为添加和读取元素都是基于锁来实现的。如果在创建队列的时候没有指定队列的容量,那么其容量就会被默认设置为Integer.MAX_VALUE,成为无界队列。

3.3 PriorityBlockingQueue

它是一个支持优先级的无界队列,默认情况下会采用自然顺序升序排列,队列中的元素可以实现Comparator接口来自定义排序规则。创建队列时可以指定数组的容量以及元素的排列顺序。

 PriorityBlockingQueue queue = new PriorityBlockingQueue(200, new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return 0;
            }
        });
3.4 DelayQueue

它是一个支持延时获取元素的无界阻塞队列,队列使用PriorityQueue来实现,队列中的元素必须实现Delayed接口。创建元素的时候可以指定过期时间,只有元素过期后才能被处理。

3.5 SynchronousQueue

它是一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的读取操作。同样任何一个读取操作必须等待另一个线程的插入操作。因此队列内部没有任何一个元素,也可以说容器的容量为0。

3.6 LinkedTransferQueue

它是一个基于链表结构的无界阻塞队列,它实现了TransferQueue接口,此接口有3个重要的方法:

  • transfer(E e):如果当前有空闲线程能够处理任务,则立即将任务交给空闲线程来处理。如果没有空闲线程来处理任务,则将元素插入到队列尾部,当前线程进入阻塞状态,直到有线程将任务取走。
  • tryTransfer(E e):如果当前有空闲线程能够处理任务,则立即将任务交给空闲线程来处理,如果没有则返回false,并且不进入队列
  • tryTransfer(E e, long timeout, TimeUnit unit):如果当前有空闲线程能够处理任务,则立即将任务交给空闲线程来处理。如果没有空闲线程,则将任务插入到队列尾部,并且等待线程处理。如果在指定的时间内有线程处理则返回true,否则返回false。
3.7 LinkedBlockingDeque

它是一个基于链表结构的双向阻塞队列。双向队列可以从队列的两端插入和移除元素。因此在由多线程同时入队时,也就减少了一半的竞争。

4.阻塞队列的实现原理

如果我们分析基本的队列实现,BlockingQueue基本上都是基于ReentrantLock锁来实现的。以ArrayBlockingQueue阻塞队列为例。

 /** Main lock guarding all access */
    final ReentrantLock lock;

    /** Condition for waiting takes */
    private final Condition notEmpty;

    /** Condition for waiting puts */
    private final Condition notFull;

上面的代码中lock是一个重入锁,notEmpty和notFull都是同一个重入锁的条件变量,notEmpty条件用于控制读取任务线程的唤醒和阻塞,而notFull条件用于控制添加任务线程的唤醒和阻塞。先看put方法

  public void put(E e) throws InterruptedException {
        Objects.requireNonNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

从上面的代码可以看出它,在插入元素的时候首先获取一个可中断锁,然后判断如果队列已满,则阻塞线程。当线程被其他线程唤醒的时候,通过enqueue(E x)方法将元素插入队列。和offer()方法的区别是,offer()方法在插入元素的时候如果队列已满则直接返回false,不会阻塞线程。再看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) putIndex = 0;
        count++;
        notEmpty.signal();
    }

enqueue()方法用于将元素放入队列,将任务放到队列以后即有任务需要处理,通过notEmpty信号来唤醒正在等待取任务的线程来处理任务。再看take()方法:

 public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

take()方法用于读取队列中的元素,在取任务时首先判断队列中是否有元素,如果有就通过dequeue()进行出队操作,如果没有则阻塞线程。再看dequeue()方法:

  private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length) takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();
        return x;
    }

dequeue()方法用于将队列中的元素进行出队操作,当有元素出队后即队列不满,则通过notFull信号,来唤醒正在等待插入元素的线程。

你可能感兴趣的:(Java多线程)