目录
一、队列(Queue)
Queue是数据结构中比较重要的一种类型,它支持FIFO,尾部添加、头部删除(先进队列的元素显出队列)。
Queue与List、Set同一级别,都是继承了Collection接口。
Queue可以让人们有效地在尾部添加一个元素, 在头部删除一个元素。有两个端头的队列, 即双端队列,可以让人们有效地在头部和尾部同时添加或删除元素。不支持在队列中间添加元素。在 Java SE 6中引人了 Deque 接口,并由 ArrayDeque 和LinkedList 类实现。这两个类都提供了双端队列,而且在必要时可以增加队列的长度。
1.Queue接口
Queue接口设计了一些可以在队列头部删除元素、在尾部添加元素的方法。这个接口比较简单,一共只有6个方法,分别如下:
(1)添加元素:
boolean add(E e); boolean offer(E e);
//这两个方法都在尾部添加一个值为e的元素。如果队列没有满,将给定的元素添加到这个队列的尾部并返回true。不同的是如果队列满了,add方法会抛出一个IllegalStateException异常,而offer方法返回false。
(2)删除并返回元素
remove(); poll();
这两个方法删除队列头部的元素。如果队列非空,这两个方法删除头部元素后并返回这个元素。不同的是如果队列是空的,remove方法抛出一个NoSuchElementException异常,而poll方法返回false。
(3) 获得头部元素
element(); peek();
这两个方法都会返回头部的元素,而都不删除头部元素。不同的是如果队列是空的,element方法抛出一个NoSuchElementException异常,而peek方法返回null。
2.Deque接口
一般的队列只能在头部删除元素、在尾部添加元素,即只有一个端。而双端队列有两个端,支持在两端同时添加或删除元素。Deque接口是Java SE 6引入的,并由ArrayDeque类和LinkedList类实现,这两个类都提供了双端队列,并在必要的时候可以增加队列的长度。
Deque接口在Queue接口的基础之上增加了一些针对双端添加和删除元素的方法,这些方法根据出错时的行为也可以分为几组。这些方法就是在Queue接口中的方法名后面加上“First”和“Last”表明在哪端操作。这些方法整理如下:
(1)添加元素
void addFirst(E e); void addLast(E e); boolean offerFirst(E e); boolean offerLast(E e);
这四个方法在头部或尾部添加给定的元素。如果队列满了,前两个方法将抛出一个IllegalStateException异常,后两个方法返回false。
(2)删除并返回元素
removeFirst(); removeLast(); pollFirst(); pollLast();
这四个方法删除头部或尾部的元素并返回。如果队列为空,前两个方法将抛出一个NoSuchElementException异常,后两个方法返回null。
(3)返回但不删除元素
getFirst(); getLast(); peekFirst(); peekLast();
这四个方法返回头部或尾部的元素,但不删除。如果队列为空,前两个方法将抛出一个NoSuchElementException异常,后两个方法返回null。
3.ArrayDeque类
ArrayDeque采用了循环数组的方式来完成双端队列的功能。优点如下:
- 无限的扩展,自动扩展队列大小的。(当然在不会内存溢出的情况下。)
- 非线程安全的,不支持并发访问和修改。
- 支持fast-fail.
- 作为栈使用的话比比栈要快.
- 当队列使用比linklist要快。
- null元素被禁止使用。
二、非阻塞队列(AbstractQueue)
1.优先级队列(priority queue)
元素可以按照任意的顺序插人,却总是按照排序的顺序进行检索。也就是说,无论何时调用 remove 方法,总会获得当前优先级队列中最小的元素。然而,优先级队列并没有对所有的元素进行排序。如果用迭代的方式处理这些元素,并不需要对它们进行排序。优先级队列使用了一个优雅且高效的数据结构,称为堆(heap)。堆是一个可以自我调整的二叉树,对树执行添加( add) 和删除(remore) 操作, 可以让最小的元素移动到根,而不必花费时间对元素进行排序。与 TreeSet—样,一个优先级队列既可以保存实现了 Comparable 接口的类对象, 也可以保存在构造器中提供的 Comparator 对象。使用优先级队列的典型示例是任务调度。每一个任务有一个优先级,任务以随机顺序添加到队列中。每当启动一个新的任务时,都将优先级最高的任务从队列中删除(由于习惯上将 1 设为“ 最高” 优先级,所以会将最小的元素删除)。
2.ConcurrentLinkedQueue
ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法来实现,该算法在Michael & Scott算法上进行了一些修改。
ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。
三、阻塞队列(BlockingQueue)
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
阻塞队列提供了四种处理方法:
方法/处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add() | offer(e) | put() | offer(e,time, unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
检查方法 | element() | peek() | 不可用 | 不可用 |
JDK7提供了7个阻塞队列。分别是
- ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
下面主要介绍前两种:
1.ArrayBlockingQueue
ArrayBlockingQueue是一个阻塞式的队列,继承自AbstractBlockingQueue,间接的实现了Queue接口和Collection接口。底层以数组的形式保存数据(实际上可看作一个循环数组)。
ArrayBlockingQueue 是一个有界队列,有界也就意味着,它不能够存储无限多数量的对象。所以在创建 ArrayBlockingQueue 时,必须要给它指定一个队列的大小。
下面是ArrayBlockingQueue中的几个重要的方法:
- add(E e):把 e 加到 BlockingQueue 里,即如果 BlockingQueue 可以容纳,则返回 true,否则报异常
- offer(E e):表示如果可能的话,将 e 加到 BlockingQueue 里,即如果 BlockingQueue 可以容纳,则返回 true,否则返回 false
- put(E e):把 e 加到 BlockingQueue 里,如果 BlockQueue 没有空间,则调用此方法的线程被阻断直到 BlockingQueue 里面有空间再继续
- poll(time):取走 BlockingQueue 里排在首位的对象,若不能立即取出,则可以等 time 参数规定的时间,取不到时返回 null
- take():取走 BlockingQueue 里排在首位的对象,若 BlockingQueue 为空,阻断进入等待状态直到 Blocking 有新的对象被加入为止
- remainingCapacity():剩余可用的大小。等于初始容量减去当前的 size
注意:
- ArrayBlockingQueue是先进先出队列
- ArrayBlockingQueue是有界队列(即初始化时指定的容量,就是队列最大的容量,不会出现扩容,容量满,则阻塞进队操作;容量空,则阻塞出队操作)
- ArrayBlockingQueue不支持空元素
ArrayBlockingQueue 进队操作采用了加锁的方式保证并发安全。源代码里面有一个 while() 判断:
public void put(E e) throws InterruptedException { checkNotNull(e); // 非空判断 final ReentrantLock lock = this.lock; lock.lockInterruptibly(); // 获取锁 try { while (count == items.length) { // 一直阻塞,知道队列非满时,被唤醒 notFull.await(); } enqueue(e); // 进队 } finally { lock.unlock(); } } public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { checkNotNull(e); long nanos = unit.toNanos(timeout); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) { // 阻塞,知道队列不满 // 或者超时时间已过,返回false if (nanos <= 0) return false; nanos = notFull.awaitNanos(nanos); } enqueue(e); return true; } finally { lock.unlock(); } } |
2.LinkedBlockingQueue
ArrayList和ArrayBlockingQueue一样,内部基于数组来存放元素,而LinkedBlockingQueue则和LinkedList一样,内部基于链表来存放元素。
每个添加到LinkedBlockingQueue队列中的数据都将被封装成Node节点,添加的链表队列中,其中head和last分别指向队列的头结点和尾结点。与ArrayBlockingQueue不同的是,LinkedBlockingQueue内部分别使用了takeLock 和 putLock 对并发进行控制,也就是说,添加和删除操作并不是互斥操作,可以同时进行,这样也就可以大大提高吞吐量。
这里如果不指定队列的容量大小,也就是使用默认的Integer.MAX_VALUE,如果存在添加速度大于删除速度时候,有可能会内存溢出,这点在使用前希望慎重考虑。
另外,LinkedBlockingQueue对每一个lock锁都提供了一个Condition用来挂起和唤醒其他线程。
put元素原理
基本过程:
1.判断元素是否为null,为null抛出异常
2.加锁(可中断锁)
3.判断队列长度是否到达容量,如果到达一直等待
4.如果没有队满,enqueue()在队尾加入元素
5.队列长度加1,此时如果队列还没有满,调用signal唤醒其他堵塞队列
public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); // Note: convention in all put/take/etc is to preset local var // holding count negative to indicate failure unless set. int c = -1; Node<E> node = new Node<E>(e); final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); try { /* * Note that count is used in wait guard even though it is * not protected by lock. This works because count can * only decrease at this point (all other puts are shut * out by lock), and we (or some other waiting put) are * signalled if it ever changes from capacity. Similarly * for all other uses of count in other wait guards. */ while (count.get() == capacity) { notFull.await(); } enqueue(node); c = count.getAndIncrement(); if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } if (c == 0) signalNotEmpty(); } |
take元素原理
基本过程:
1.加锁(依旧是ReentrantLock),注意这里的锁和写入是不同的两把锁
2.判断队列是否为空,如果为空就一直等待
3.通过dequeue方法取得数据
3.取走元素后队列是否为空,如果不为空唤醒其他等待中的队列
public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); try { while (count.get() == 0) { notEmpty.await(); } x = dequeue(); c = count.getAndDecrement(); if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x; } |