阻塞队列定义
在普通队列先进先出的基础上做了扩展:
1)线程安全的。
2)具有阻塞的特性:a.如果针对一个已经满了的队列进行入队列,此时入队列操作就会阻塞,一直阻塞到队列有空位。b.如果针对一个已经空了的队列进行出队列,此时出队操作就会阻塞,一直阻塞到队列不空之后。
阻塞队列应用
基于阻塞队列的特性,可以实现“生产者消费者模型”,
生产者消费者模型的作用:
1)引入生产者消费者模型,就可以更好的做的“解耦合"(把代码的耦合度降低),原理是引入一个队列,把“生产者”和“消费者”分离开来。
2)削峰填谷,所谓的“峰”和“谷”就是执行指令多和执行指令少的情况。假如当用户请求多的时候,入口服务器压力就会很大,处理服务器压力会更大,甚至出现死机情况,这是因为首先入口服务器抗压能力大,处理服务器抗压能力小,而且处理服务器在处理每个请求时都会消耗硬件资源,包括但不限于cpu,内存,网络带宽....,即使一个请求需要的资源很少,但当请求资源很多时,总的资源就很多了,当任意一个资源达到上限,处理服务器就会挂掉。
而“生产者消费者模型”通过“消息队列(也可以称作阻塞队列,侧重点不同罢了)”来承担峰值请求,处理服务器仍按照原来的速度来处理请求。而队列没什么业务逻辑,只是存储数据,所以抗压能力很强,不会轻易挂掉。
引入阻塞队列的代价:
1)阻塞队列不是简单的数据结构,而是基于这个数据结构实现的服务器程序,又被部署到单独的主机上了。
2)整个系统的结构更复杂了,要维护的服务器更多了。
3)效率,引入了中间商,还有差价,指令经过队列的转发,这个过程也是有一定的开销的。
JAVA中的阻塞队列
JAVA中提供了现成的阻塞队列数据结构:
BlockingQueue是一个interface,实现的类有ArrayBlockingQueue;LinkedBlockingQueue;PriorityBlokingQueue.
public static void main(String[]args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
queue.put("aaa");//入队列
String s = queue.take();
System.out.println(s);
String s1 = queue.take();
System.out.println(s1);
}
这段代码就是基于ArrayBlockingqueue的实现例子,运行后,只有s打印,打印s1时就会陷入阻塞状态。但阻塞队列不提供获取队首元素的方法。
自己实现阻塞队列:
1)先实现普通队列
2)再加上线程安全。
3)再加上阻塞功能。
对于1,基于数组实现(环形队列)
要实现环形队列有两种方法:
1)浪费一个格子,tail最多走到head前一个位置。
2)引入size变量。
这里使用的是引入size变量
class MyBlockingQueue{
private String[] elems = null;
private int tail=0;
private int head = 0;
private int size = 0;//队列中元素的数量
public MyBlockingQueue(int capacity){
elems = new String[capacity];
}
public void put(String elem){
if(size>= elems.length){
return;
}
elems[tail] = elem;
tail++;
if(tail>=elems.length){
tail=0;
}
size++;
}
public String take(){
if(size == 0){
return null;
}
String elem = elems[head];
head++;
if(head>=elems.length){
head=0;
}
size--;
return elem;
}
}
2.引入线程安全问题
引入锁,解决线程安全问题。
put是写操作,肯定要加入锁操作。
public void put(String elem){
if(size>= elems.length){
return;
}
elems[tail] = elem;
tail++;
if(tail>=elems.length){
tail=0;
}
size++;
}
加锁后
public void put(String elem){
synchronized (lock) {
if (size >= elems.length) {
return;
}
elems[tail] = elem;
tail++;
if (tail >= elems.length) {
tail = 0;
}
size++;
}
}
与之相同,take方法也需要加锁
public String take(){
String elem = null;
synchronized (lock){
if(size == 0){
return null;
}
elem = elems[head];
head++;
if(head>=elems.length){
head=0;
}
size--;
lock.notify();
}
return elem;
}
3.加入阻塞
使用wait来实现阻塞!
当队列满时,添加wait,当队列不满时,加上唤醒。也就是在take方法加上notify方法,在put方法里加上wait方法。
public void put(String elem) throws InterruptedException {
synchronized (lock){
if(size>= elems.length){
lock.wait();
}
elems[tail] = elem;
tail++;
if(tail>=elems.length){
tail=0;
}
size++;
}
}
public String take(){
String elem = null;
synchronized (lock){
if(size == 0){
return null;
}
elem = elems[head];
head++;
if(head>=elems.length){
head=0;
}
size--;
lock.notify();
}
return elem;
}
当队列空了,再出队列,同样也需要阻塞,而且同样是在另一个入队列成功后的线程中唤醒。
public void put(String elem) throws InterruptedException {
synchronized (lock){
if(size>= elems.length){
lock.wait();
}
elems[tail] = elem;
tail++;
if(tail>=elems.length){
tail=0;
}
size++;
lock.notify();
}
}
public String take() throws InterruptedException {
String elem = null;
synchronized (lock){
if(size == 0){
lock.wait();
}
elem = elems[head];
head++;
if(head>=elems.length){
head=0;
}
size--;
lock.notify();
}
return elem;
}
而且队满和队空的情况都要用同一个锁,不然会出现多一个元素的情况。
但是代码此时仍存在问题,比如当队列已满,此时出队一个元素就会唤醒put的锁,但是如果线程1的put进行完如下代码后被线程2抢占了cpu。
synchronized (lock){
if(size>= elems.length){
lock.wait();
}
此时由于线程1执行了wait操作,已经归还了锁,此时线程2的put操作就可以抢到锁,如果线程2的put操作执行完了的话,队列此时其实已经是满了的,但是线程1由于已经进行了if判断,就会误认为队列没满,然后还会入队元素,导致错误。
解决方法就是将if判断改为while判断,一直判断,如果有线程抢先完成了入队或出队操作,就不会再次进行入队或出队操作。
while(size>= elems.length){
lock.wait();
}
while(size == 0){
lock.wait();
}
基于这个阻塞队列写一个简单的生产者消费者模型
public static void main(String[]args) {
MyBlockingQueue myBlockingQueue = new MyBlockingQueue(1000);
Thread t1 =new Thread(()->{
int n=1;
while(true){
try {
myBlockingQueue.put(n+"");
System.out.println("生产元素"+n);
n++;
//Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread t2 = new Thread(()->{
while(true){
try {
String n = myBlockingQueue.take();
System.out.println("消费元素"+n);
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}