目录
判断head,和tail是否走到队尾的时候,将数组循环起来的时候,两种代码分析:
什么是阻塞队列?
阻塞队列是一种特殊的队列,遵循“先进先出”原则。
阻塞队列也是一种线程安全的数据结构,并遵循如下的原则:
当队列满的时候,再要入队列时,就会处于阻塞等待的状态,直到有其他线程出队列;
当队列空的时候,要再出队列,也会处于阻塞等待的状态,直到有其他线程入队列。
⽣产者消费者模型:
⽣产者消费者模式 就是通过⼀个容器来解决⽣产者和消费者的强耦合问题。
⽣产者和消费者彼此之间不直接通讯,⽽通过阻塞队列来进⾏通讯,所以⽣产者⽣产完数据之后不⽤ 等待消费者处理,直接扔给阻塞队列,消费者不找⽣产者要数据,⽽是直接从阻塞队列⾥取。
阻塞队列的功能:
1.阻塞队列相当于一个缓冲区,平衡了生产者与消费者的处理能力。(肖锋填谷)
:当生产者生产产品过多,中间有一个缓冲的流程,来让消费者慢慢消费。阻塞队列类似与水利工程中的三峡大坝。
2.阻塞队列也使生产者与消费者 解耦。
有了缓冲的中间过程,因此,生产者与消费者之间就不是直接联系的,就算消费者添加或减少,生产者也无需变动。
java标准库中阻塞队列的数据结构:
在Java标准库中内置了阻塞队列.如果我们需要在⼀些程序中使⽤阻塞队列,直接使⽤标准库中的即 可.
• BlockingQueue是⼀个接口,真正实现的类是LinkedBlockingQueue.
• put⽅法⽤于阻塞式的⼊队列,take⽤于阻塞式的出队列.
• BlockingQueue也有offer,poll,peek等⽅法,但是这些⽅法不带有阻塞特性.
后面这些类都是BlockingQueue接口的实现方法,都实现了阻塞队列功能,只不过,插入/取出元素时,要用put和tabke方法,才能实现阻塞功能.
代码展示:
/**
* 阻塞队列
*/
public class Thread20 {
public static void main(String[] args) throws InterruptedException {
// PriorityBlockingQueue queue = new PriorityBlockingQueue();
LinkedBlockingQueue queue = new LinkedBlockingQueue();
queue.put(1);
queue.put(2);
queue.put(3);
queue.put(4);
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
}
}
用循环数组 模拟实现阻塞队列:
先实现其基本功能:
/**
* 模拟实现阻塞队列
* 通过 循环数组队列 实现
*
* head:指向队头
* tail:指向队尾
* size:记录数组中元素的个数
*/
class MyPriorityQueue1{
public String[] elem=null;
public int head;
public int tail;
public int size;//数组中元素个数
public MyPriorityQueue1(int capacity){
elem=new String[capacity];
}
//往数组中放元素
public void put(String s){
if(size==elem.length){
//队列已满,执行阻塞功能
}
elem[tail]=s;//尾插到队列中
tail++;
if(tail==elem.length){
tail=0;//当tail走到队尾时,让他从头开始,循环起来
}
size++;
}
//从数组中取元素
public String take(){
if(size==0){
//队列为空,无法再取元素,执行阻塞功能
}
String s=elem[head];
head++;
if(head==elem.length){
head=0;//当head走到队尾时,让他从头开始,循环起来
}
size--;
return s;
}
}
若是多线程下,该队列是不安全的,存在线程安全问题,要通过synchronized和wait/notify来实现多线程下的功能。
对put作下面的操作:
1.put方法中,整个过程都有对数据的修改,可能存在线程安全问题 ,需要将整个方法打包成一个原子(将锁加在方法名处,或加在方法体中,效果都是一样的)
2.当队列已满,再次插入元素时,执行wait阻塞功能.
3.插入元素后,队列一定不为空,调用notify唤醒别的线程的因 队列为空 被阻塞的take线程.
4.防止在多线程中,一个线程中 put 在wait阻塞时,而释放锁,唤醒另一个线程的put锁,在判断是否要阻塞时,将其设为while循环,设置多次判断,防止唤醒后,值已经被修改,引发线程安全问题.
关于多线程条件下,将if改成while
详细解释分析:
此外,在使用wait时,强调不仅要与synchronized一起使用,还要与while判断一起使用.
take方法也要修改,修改位置和修改原因和put等同:
判断head,和tail是否走到队尾的时候,将数组循环起来的时候,两种代码分析:
这两种方式的功能是相同的,第二中,用%的方式,当tail<数组长度是,就是将自己赋给自己,要等于数组长度,%,就为0了,不可能大于数组长度,
从执行效率上看,判断属于条件跳转语句,执行速度是非常快的,在大多数情况下,不会触发赋值操作.且从理解程度上开看,判断语句,非常容易理解其作用.
取模%,本质上是除法运算指令,在cpu中对 + -运算速率较快,但对于* / ,相比,速度就慢了,(计算机组成原理中有涉及到相关计算底层执行流程),且第二种方式,不论是否到队尾,都要执行赋值操作.
判断一个代码的好坏:
1>.代码是否容易理解.
2>.代码效率的高低.
相比之下,第一种方式略好一些.
参考代码:
/**
* 模拟实现阻塞队列
* 通过 循环数组队列 实现
*
* head:指向队头
* tail:指向队尾
* size:记录数组中元素的个数
*/
class MyPriorityQueue1{
public String[] elem=null;
public int head;
public int tail;
public int size;//数组中元素个数
public MyPriorityQueue1(int capacity){
elem=new String[capacity];
}
Object locker=new Object();
/**
//在多线程下 往数组中放元素
//因put方法中,整个过程都有对数据的修改,可能存在线程安全问题
//需要将整个方法打包成一个原子
* @param s 要插入的元素
* @throws InterruptedException
*/
public void put(String s) throws InterruptedException {
synchronized(locker){
//这里要将if改成while,多次判断
//防止多线程下,一个线程中 put在wait阻塞时,而释放锁,唤醒另一个线程的put
//这里设置多次判断,防止唤醒后,值已经被修改,引发线程安全问题
while(size==elem.length){
//队列已满,再次插入元素是,执行阻塞功能
locker.wait();
}
elem[tail]=s;//尾插到队列中
tail++;
if(tail==elem.length){
tail=0;//当tail走到队尾时,让他从头开始,循环起来
}
size++;
//放入元素后,队列一定不再是空,唤醒 因队列为空而阻塞的take线程
//(这里也有可能唤醒别的线程)
locker.notify();
}
}
//从数组中取元素
/**
* 在多线程下 从数组中取元素
* take方法中,整个过程都有对数据的修改,可能存在线程安全问题
* 也需要将 整个方法 打包成一个 原子
* @return
* @throws InterruptedException
*/
public String take() throws InterruptedException {
synchronized(locker){
//这里也要修改,和put中修改原因相同
while(size==0){
//队列为空,再取元素时,执行阻塞功能
locker.wait();
}
String s=elem[head];
head++;
if(head==elem.length){
head=0;//当head走到队尾时,让他从头开始,循环起来
}
size--;
//取出元素后,队列一定不再是满,唤醒 因队列已满而阻塞的put线程
//(这里也有可能唤醒别的线程)
locker.notify();
return s;
}
}
}