阻塞队列
什么是阻塞队列?
阻塞队列是一种特殊的队列,它具有阻塞的特性。当队列为空时,从队列中获取元素的操作将会被阻塞,直到队列中有元素可用;当队列已满时,往队列中添加元素的操作将会被阻塞,直到队列中有空闲位置。
自己实现一个简单的阻塞队列
class MyBlockingQueue3 {
//阻塞队列的容量为10
volatile private String[] array = new String[10];
//设置一个计数器,用来记录元素的数量
volatile private int count = 0;
//头,尾指针分别用来拿出和添加任务
volatile private int head = 0;
volatile private int tail = 0;
//定义一个锁,用来解决线程安全问题
private Object locker = new Object();
//take方法将任务s添加到阻塞队列当中
public void put(String s) throws InterruptedException {
synchronized (locker) {
//当count等于数组长度时,入队列阻塞,当count等于0时出队列阻塞
while(count >= array.length) {
locker.wait();
}
array[tail] = s;
count ++;
tail ++;
//底层逻辑为一个循环队列,设置前后指针,当俩个指针移动到超出数组的位置时
//说明要开始一个新的循环,让其指到0
if(tail == array.length){
tail = 0;
}
locker.notify();
}
}
//put方法将入阻塞队列最早的元素取出来
public String take() throws InterruptedException {
synchronized (locker) {
while(count == 0) {
locker.wait();
}
String cur = array[head];
head ++;
count --;
if(head == array.length) {
head = 0;
}
locker.notify();
return cur;
}
}
}
实现阻塞:队列满的时候,put方法阻塞,直到take方法拿出元素时唤醒
队列为空的时候,take方法阻塞,直到put方法放进元素时唤醒
队列不可能同时为空和为满,所以就实现了上述put方法和take方法的交叉唤醒
基于多线程安全问题的分析:
1:put 和 take多线程调用时会产生线程安全问题
解决方法:synchronized加锁
2:变量的内存可见性问题
解决方法:volatile关键字
***3.wait的使用习惯
wait一定是被notify唤醒吗?不一定,他还有可能被interrupt唤醒,在不该被唤醒的时候被唤醒了,还有可能其他线程调用了错误的notify,这就叫做虚假唤醒,即违背了我们本来的想法,产生的错误的唤醒。
解决方法:在使用wait时给他嵌套一个while循环的判断,每次被唤醒后进行条件判断,如果不满足就说明是一个虚假的唤醒,就让它继续wait
Java的开发者也是这样建议的:
生产者消费者模式
生产者-消费者模式是一种常见的并发编程模型,它用于解决生产者和消费者之间的数据共享和同步问题。在生产者-消费者模式中,生产者负责生成数据并将其放入共享缓冲区,消费者负责从缓冲区中获取数据并进行处理。这种模式通常用于多个线程之间的协作,以实现高效的数据处理和资源利用。
阻塞队列的一个主要应用就是生产者消费者模型,其共享缓冲区就是由阻塞队列实现,生产者将数据放入阻塞队列中,消费者从队列中获取数据进行处理,当队列为空时消费者线程会被阻塞,当队列已满时生产者线程会被阻塞。
生产者消费者模式的作用
1:解耦合
生产者和消费者之间并无关联,他们之间并不直接交流,通过数据缓冲区进行数据交换。
2:缓冲
当任务量急增时,如果一股脑交给服务器执行,可能会造成崩溃,阻塞队列将这些任务先存放起来,让服务器在能力范围内慢慢执行。
库中提供的阻塞队列
1.ArrayBlockingQueue:基于数组实现的有界阻塞队列,当队列已满时,插入元素的线程会被阻塞,直到有空闲位置为止。
2.LinkedBlockingQueue:基于链表实现的可选有界阻塞队列,当队列已满时,插入元素的线程会被阻塞,直到有空闲位置为止。如果不指定队列大小,则默认为无界队列。
3.SynchronousQueue:没有容量的阻塞队列,每个插入操作必须等待另一个线程的移除操作,反之亦然。因此,SynchronousQueue通常用于线程之间的直接传递。
4.PriorityBlockingQueue:基于优先级堆实现的无界阻塞队列,支持元素按照优先级进行排序。
5.DelayQueue:基于PriorityQueue实现的延迟队列,其中的元素只有在指定的延迟时间到达后才能被取出。
定时器
实现在规定时间执行某个任务的组件
在定时器中有Timer和TimerTask类,TimerTask用来描述待执行任务,Timer用来调度管理这些任务,Timer中有schedule方法用来调度待执行的任务
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
// 执行任务操作
}
};
//delay是待执行任务的延迟时间,单位为毫秒
timer.schedule(task, delay);
基于此我们可以自己尝试实现一个简单的定时器:
import java.util.PriorityQueue;
//TimerTask用来描述待执行的任务,并让其实现Comparable接口,使其在入队列时按照待执行时间排序
class TimerTask2 implements Comparable<TimerTask2>{
private long time;
private Runnable runnable;
//将输入的任务的延迟时间转化为执行时间
public TimerTask2(Runnable runnable,long delay) {
this.time = delay + System.currentTimeMillis();
this.runnable = runnable;
}
public long getTime() {
return time;
}
public Runnable getRunnable() {
return runnable;
}
@Override
public int compareTo(TimerTask2 o) {
return (int) (this.time - o.time);
}
}
class MyTimer2 {
private PriorityQueue<TimerTask2> priorityQueue = new PriorityQueue<>();
private Object locker = new Object();
public MyTimer2() throws InterruptedException {
Thread thread = new Thread(() -> {
try {
//设置一个永远为真的循环,让线程一直在运转
while(true) {
synchronized(locker) {
//栈为空就陷入等待
while (priorityQueue.isEmpty()){
locker.wait();
}
//如果现在的时间小于等于任务规定的时间,就立刻执行并出栈,否则计算还需要等待的时间然后按照这个时间wait
if(priorityQueue.peek().getTime() > System.currentTimeMillis()){
locker.wait(priorityQueue.peek().getTime() - System.currentTimeMillis());
}else {
priorityQueue.poll().getRunnable().run();
}
}
}
}catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread.start();
}
public void schedule(Runnable runnable, long time){
synchronized (locker) {
TimerTask2 timerTask2 = new TimerTask2(runnable, time);
priorityQueue.offer(timerTask2);
//每次向堆中添加一个对象就唤醒锁,让线程重新检查堆里最迫切需要执行的任务
locker.notify();
}
}
}
如何让任务在该执行的时间执行呢?
效率低的做法是以线性表的方式组织任务并不停地对线性表进行遍历。这会消耗大量cpu资源。
实际上,我们不需要关注每个任务,我们只需要关注最迫切需要执行的任务。这个任务没有到时间,其它任务就不可能到时间。
所以我们使用堆的方式对待执行任务进行组织,让堆顶元素为最迫切执行的任务,然后对它进行监控就行了。
MyTimer的构造方法对栈顶元素进行检查,如果为空就进行wait,直到添加进新的元素。否则检查栈顶元素的执行时间,到时间或者超时就立即执行,没到时间就进行wait,没到执行时间的wait会被添加进新的任务唤醒,因为最迫切执行任务可能更新为这个新添加的任务,所以要重新检查。
注意事项:
1.在对堆进行操作时不是线程安全的,需要加锁:
2.线程休眠可以用sleep吗?
答案是不行;
1.定时器的休眠会随着添加进新的任务中断并重新进行栈顶元素的检查,但是sleep的休眠被中断意味着线程应该结束了,会抛出异常。
2.sleep不会释放锁,所以在等待任务时间的过程中就不能进行添加任务的操作了,影响并发性。
所以,sleep在这里使用是不合适的。