目录
队列
基本模型
表示一组数据的集合,按照规则排列和访问
queue是先进先出FIFO
基本的数据模型包含
- 一个存放数组的列表
- 头尾指针
入队:存入tail指向索引位置,tail指针后移一位
出队:取出head指向索引位置,设置为null,head指针后移一位
Java 接口-队列
java提供顶层接口Queue
方法 | 返回 | 描述 |
---|---|---|
add | boolean | 有空位插入返回true,否则异常 |
offer | boolean | 有空位插入返回true,否则返回false |
remove | E | 弹出头部节点内容,如果为空则异常 |
poll | E | 弹出头部节点内容,如果为空则返回null |
element | E | 返回头部节点内容,不弹出,如果为空则异常 |
peek | E | 返回头部节点内容,不弹出,如果为空则返回null |
Java 接口-阻塞队列
调用出队,入队会发生阻塞的队列接口
线程安全
看一下非继承的方法:
方法 | 返回 | 描述 |
---|---|---|
put | void | 插入,并在需要时等待 |
offer(E, timeOut,timeUtil) | boolean | 插入,并在需要时等待指定的时间,如果超时还没有可用则返回false |
take | E | 弹出头部节点内容,如果有需要则等待 |
poll | E | 弹出头部节点内容,如果有需要则进行有超时时间的等待,如果超时还没有可用则返回null |
remainingCapacity | int | 返回当前剩余容量 |
drainTo | int | 把剩余可用元素转移到另外一个集合,返回转移元素数 |
drainTo(Collection,int) | int | 把指定个数元素转移到另外一个集合,返回转移元素数 |
实现 ArrayBlockingQueue
基于数据实现的阻塞队列
成员变量
头尾指针只维护队列中的索引
通过ReentrantLock和2个条件来保证并发安全
因为阻塞队列的元素个数是固定的
所以
在队列容量满了的时候入队操作就会等待
在队列容量空了的时候出队操作就会等待
生产、消费者模型
入队 put
获取锁
如果队列满了就阻塞在notFull条件上(注意这里使用的是while,避免虽然唤醒了,但是其实没有位置入队)
没满就入队
解锁
x存入tail索引中
tail索引+1,如果==容量则tail索引置为0
:这里不会有问题,因为在上边已经判断如果容量满了就阻塞了
有效数量+1
唤醒notEmpty条件的一个线程
出队 take
获取锁
如果可用量为0
则阻塞在notEmpty条件上
否则出队
取出head索引元素给x
设置head索引元素为null
head索引+1,如果到达容量数则设置head索引为0
有效数量-1
唤醒阻塞在notFull条件上的一个线程
总结
其实就是一个回环链表
在head索引处插入,如果满了就阻塞
弹出tail索引处元素,如果空了就阻塞
插入了就唤醒弹出
弹出了就唤醒插入
缺点
出入对线程使用一把锁,等于只能串行操作
实现 LinkedBlockingQueue
是由Node组成的单向队列
有头节点指针和尾节点指针
有出队锁和条件notEmpty
有入队锁和条件notFull
还有一个当前有效节点数count
初始化
入队 put
获取入队锁
循环判断:有效数量==容量就阻塞到notFull条件上
否则enqueue
count = count+1
判断:count + 1 < 容量则唤醒notFull条件上
解锁
如果count==0
则唤醒notEmpty条件
这里是给消费者一个唤醒机会,这里会获取出队锁
这里第一步把原last的next指向入队的node
然后把last指向入队的node
出队 take
获取出队锁
循环判断:有效Node数==0则阻塞在notEmpty条件上
否则出队
count = count-1
如果count > 1则唤醒notEmpty条件
解锁
如果count==容量
则唤醒notFull条件
这里是给生产者一个唤醒机会,这里会获取入队锁
原head的next指向自己
然后返回head的next的item
head指向原head的next
分析
入队出队锁拆分
有效数量采用原子操作
提高了性能
DelayQueue
在工作中往往会遇到这样的情况,就是一个任务的执行时间是计算后的一个时间,各个任务计算后的时间并不一样,有的早执行,有的晚执行,此时就可以用延时队列
现在来看下使用方法:
知道怎么使用后来看下具体的实现
有一个PriorityQueue作为容器
初始容量为11
可以设置自定义比较器
入队 offer
获取锁
插入PriorityQueue中
如果PriorityQueue优先级最高的元素就是当前元素
则设置leader为null
唤醒阻塞在available条件上的线程
返回为true
解锁
PriorityQueue入队
改动次数(modCount)+1
如果有效数量>=队列容量则扩容
如果有效数=0则插入队首
否则计算后插入
有效数+1
返回true
使用默认比较器的入队
循环比较:
获取父类的索引,(index-1)/2
如果比父节点大或相等,则跳出
如果比父节点小,则把父节点塞入当前位置,index位设置为父索引,进入下一次循环
跳出后:将元素设置到index索引位置
总结:不保证队列的顺序性,但是保证第一个接口一定是最小的
并且没有挨个比较提升了性能
并且在移位的时候使用的是无符号右移,不会出现小数
那就有个问题既然不保证顺序性那下一次取数据的时候岂不是乱了?
其实是在每次出队的时候会重新再计算下最小值具体在 poll代码里。
出队take
获取锁
循环:
取出第一个
如果为null,则等待在available条件上
否则获取其延时剩余时间
如果小于0则出队返回
判断leader不为null,则等待在available条件上
否则设置leader为当前线程并await延时时间
延时结束后判断当前线程如果是leader的话设置leader为null
结束后判断如果leader==null并且head不为null,则唤醒available条件上的队列
解锁
poll()
在返回前进行了切换, 从0开始重新检查最小值
总结
从代码可以看出,如果线程一旦获取到锁就会设置leader
如果取出来的第一个ok则出队
如果不ok则await,重新进入循环,下次循环取一下head重新去判断
注意这里其他线程获取的任务如果已经ok 则不会进入leader机制
否则不是leader就直接进入等待,避免了线程安全问题