2019-05-18 阻塞队列和并发容器
1.什么是阻塞队列
阻塞队列(BlockingQueue)是指当队列为空时,取出元素的操作会一直阻塞,直到队列有元素。当队列满时,插入元素会阻塞,直到队列恢复非满可用状态。
2.java里的阻塞队列
JDK提供7个阻塞队列,分别是:
- ArrayBlockinigQueue: 一个由数据组成的有界阻塞队列。
- LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列。
- DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue: 一个不存储元素的阻塞队列。
- LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
- LinkedBlockingQueue: 一个由链表结构组成的双向阻塞队列。
下面介绍几种常见的阻塞队列:
ArrayBlockingQueue
ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。我们可以使用以下代码创建一个公平的阻塞队列:
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
LinkedBlockingQueue
LinkedBlockingQueue是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE,此队列按照先进先出的原则对元素进行排序。
PriorityBlockingQueue
PriorityQueue是一个支持优先级的无界队列。默认情况下元素采取自然排序,也可以通过比较器comparator来执行元素的排序规则。
DelayQueue
DelayQueue是一个支持演示获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delay接口,在创建元素时可以指定多久才能从队列中获取当前的元素。只有延时期满的元素才能从队列中获取。我们可以将DelayQueue运用在以下应用场景:
- 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
- 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。
使用例子
package com.demo.contrainer;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class DelayQueueTest {
class DelayEle implements Delayed{
public long delayTime;
public String name;
public DelayEle (int delayTime, String name){
this.delayTime = System.currentTimeMillis() + delayTime;
this.name = name;
}
public int compareTo(Delayed o) {
return this.delayTime - ((DelayEle)o).delayTime > 0 ? 1 :
this.delayTime - ((DelayEle)o).delayTime < 0 ? -1 :0;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.delayTime - System.currentTimeMillis() , TimeUnit.MILLISECONDS);
}
public String toString(){
return this.name;
}
}
public static void main(String[] args) throws InterruptedException {
DelayQueueTest t = new DelayQueueTest();
//这里第一个参数是在多少毫秒才能被取出
DelayEle ele1 = t.new DelayEle(1000,"t1");
DelayEle ele2 = t.new DelayEle(5000,"t2");
DelayEle ele3 = t.new DelayEle(3000,"t3");
DelayQueue dq = new DelayQueue();
dq.add(ele1);
dq.add(ele2);
dq.add(ele3);
while(dq.size() > 0){
System.out.println(dq.take());
}
}
}
3.阻塞队列中的方法 VS 非阻塞队列中的方法
1.非阻塞队列中的几个主要方法:
add(E e):将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则会抛出异常;
remove():移除队首元素,若移除成功,则返回true;如果移除失败(队列为空),则会抛出异常;
offer(E e):将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则返回false;
poll():移除并获取队首元素,若成功,则返回队首元素;否则返回null;
peek():获取队首元素,若成功,则返回队首元素;否则返回null
对于非阻塞队列,一般情况下建议使用offer、poll和peek三个方法,不建议使用add和remove方法。因为使用offer、poll和peek三个方法可以通过返回值判断操作成功与否,而使用add和remove方法却不能达到这样的效果。注意,非阻塞队列中的方法都没有进行同步措施。
2.阻塞队列中的几个主要方法:
阻塞队列包括了非阻塞队列中的大部分方法,上面列举的5个方法在阻塞队列中都存在,但是要注意这5个方法在阻塞队列中都进行了同步措施。除此之外,阻塞队列提供了另外4个非常有用的方法:
put(E e)
take()
offer(E e,long timeout, TimeUnit unit)
poll(long timeout, TimeUnit unit)
- put方法用来向队尾存入元素,如果队列满,则等待;
- take方法用来从队首取元素,如果队列为空,则等待;
- offer方法用来向队尾存入元素,如果队列满,则等待一定的时间,当时间期限达到时,如果还没有插入成功,则返回false;否则返回true;
- poll方法用来从队首取元素,如果队列空,则等待一定的时间,当时间期限达到时,如果取到,则返回null;否则返回取得的元素;
4.并发容器 copy-on-write(COW)
1.CopyOnWirte容器即写时复制的容器,就是当容器添加元素的时候,先将容器复制一份,然后往新的容器里面添加元素,然后再将原容器指向新容器。JDK1.5以后,java并发包提供了两个使用CopyOnWrite机制实现的并发容器:CopyOnWriteArrayList和CopyOnWriteArraySet。
2.优点:容器支持并发的读,不需要加锁。适用于读多写少的并发场景。
3.缺点:内存占用问题以及无法保证数据一致性。
5.并发容器ConcurrentHashMap
1.锁分段技术:ConCurrentHashMap容器里有多把锁,每一把锁用于锁容器其中一部分数据,当多线程范文容器中不同数据段的数据是,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率。
2.ConcurrentHashMap的分段segment数组长度必须是2的n次方,这样的好处是方便采用移位操作来进行hash,加快hash的过程。
3.Segment的数据结构:
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile int count;
transient int modCount;
transient int threshold;
transient volatile HashEntry<K,V>[] table;
final float loadFactor;
}
Segment里面的成员变量的意义:
count:Segment中元素的数量
modCount:对table的大小造成影响的操作的数量(比如put或者remove操作)
threshold:阈值,Segment里面元素的数量超过这个值依旧就会对Segment进行扩容
table:链表数组,数组中的每一个元素代表了一个链表的头部
loadFactor:负载因子,用于确定threshold
4.ConcurrentHashMap的get操作是不用加锁的,put操作是加锁完成的.