一、💛
如何实现一个线程安全的阻塞队列
目前,当前代码是循环队列(还没有进行改动)
head和tail的判空判断满两种方法:
1.浪费一个格子,当前走到head的前一个位置,就认为队列满的
2.单独搞一个变量,去记录当前的元素个数 (下面的代码是使用的这个)
class MyBlockingQueue{ private String[] items=new String[1000]; //队列的头 private int head=0; //描述队列的下一个元素,也就是正常队列的队尾 private int tail=0; //统计多少个数量 public int size=0; //插入 public void put(String elem){ if(size>=items.length){ return ; } items[tail]=elem; tail++; if(tail>=items.length){ //这一步看我下面解析 tail=0; } size++; } //取出队头元素 public String take(){ if(size==0){ return null; } String elem=items[head]; head++; if(head> items.length){ //这一步看我下面解析 head=0; } size--; return elem; } }
在上述代码中,
if(tail>=items.length){
tail=0;
}这个代码和我们平时的循环队列是不是发现有点不一样,我们平时的选择是下面这个tail=(tail+1)%item.length 但是你实际一想,他不就是把越界的给恢复到从0开始吗,所以结果一致,而且 ,我们写代码是两个目的:
1.让写出来的代码,开发效率高(好去理解,好修改)
2.让写出的代码,运行效率高(执行速度快) 另外再补充一个小知识。cpu运算加,减,位运算,是比乘除更快一些的,乘除假如是针对(2的幂)的乘除运算,可以被优化成为位运算。
二、💙
改造(上面写的战损版)阻塞队列:
1.线程安全问题:加锁解决(直接无脑加锁(打包成原子),修改变量(count++这个类型的)的地方太多了)
2.内存可见性问题,一个线程在改,另一个线程读,内存可见性是否能够观察到修改
3.阻塞等待,wait,notify(让他等待,而不是直接返回值啥的)(插入的时候,假如说满就让他等待一会,直到你出队列,空的时候就等一会,直到你插入队列。)
class MyBlockingQueue{ private String[] items=new String[1000]; //队列的头 private volatile int head=0; //描述队列的下一个元素,也就是正常队列的队尾 private volatile int tail=0; //统计多少个数量 public volatile int size=0; //插入 public void put(String elem) throws InterruptedException { synchronized (this) { while(size >= items.length) { this.wait(); //1号(满了就阻塞等待 4号会来唤醒1号) } items[tail] = elem; tail++; if (tail >= items.length) { tail = 0; } size++; this.notify(); //2号(2号来唤醒,3号) } } //取出队头元素 public String take() throws InterruptedException { synchronized (this) { while(size == 0) { this.wait(); //3号(空了,就阻塞等待,等2号) } String elem = items[head]; head++; if (head > items.length) { head = 0; } size--; this.notify(); //4号(4唤醒1号 return elem; } } }
使用wait时候,建议搭配while使用
原因:put操作之后因为队列满,进入wait阻塞了,但是过一会wait被唤醒,唤醒的时候,队列一定就是非满状态吗,万一唤醒还是满着的,此时继续执行,不就会把之前的元素覆盖了吗。
wait是一定被notify唤醒吗?
在当前代码中,如果是interrupet唤醒会报异常,方法就会结束了,但是也就不会导致刚才说的覆盖已有元素问题。但是⚠️ 假如我拿try catch阁下该如何应对捏?——一旦是被interrupt唤醒,catch执行完毕,继续执行,也就触发了“覆盖元素”的逻辑(我们这个抛异常操作是碰巧蒙对了)
所以解决方法,拿while更加万无一失
最终下图的代码:也会理解前面的一些东西
import java.util.Scanner; import java.util.concurrent.*; class MyBlockingQueue{ private String[] items=new String[1000]; //队列的头 private volatile int head=0; //描述队列的下一个元素,也就是正常队列的队尾 private volatile int tail=0; //统计多少个数量 public volatile int size=0; //插入 public void put(String elem) throws InterruptedException { synchronized (this) { if(size >= items.length) { this.wait(); //1号 } items[tail] = elem; tail++; if (tail >= items.length) { tail = 0; } size++; this.notify(); //2号 } } //取出队头元素 public String take() throws InterruptedException { synchronized (this) { if (size == 0) { this.wait(); //3号 } String elem = items[head]; head++; if (head > items.length) { head = 0; } size--; this.notify(); //4号 return elem; } } } public class Demo { public static void main(String[] args) { MyBlockingQueue myBlockingQueue=new MyBlockingQueue(); Thread t1=new Thread(()->{ int count =0; while (true){ try { myBlockingQueue.put(String.valueOf(count)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("生产了元素"+count); count++; } }); Thread t2=new Thread(()->{ while(true) { Integer n = null; try { n = Integer.valueOf(myBlockingQueue.take()); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("消费者" + n); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); t2.start(); } }
和前面的生产者消费者模型,但是我想说的是,控制节奏,t1会快速执行到1000,后按照t2的sleep的节奏一点一点生产。
三、💜
定时器:日常开发中,常用到的组件(前后端都要用的定时器,类似于一个闹钟)
Timer类(util包)
public class Demo1 { public static void main(String[] args) { Timer timer=new Timer(); timer.schedule(new TimerTask() { //给Timer中注册的任务,不是在调用 @Override schedule的线程中执行的,而是通过Timer public void run() { 内部的线程来执行的 System.out.println("hello"); } },3000); //过了这些时间才开始执行的 } }
注意:schedule可以设置多个任务,任务的完成先后是根据等待时间的
四、💚
如何我们自己实现一个定时器(闹钟):首先先要把任务描述出来,再用数据结构来把多个任务组织起来。
1.创建一个TimerTask这样的类,表示一个任务,该任务需包含两方面,任务的内容,任务的实际执行时间->通过时间戳表示:在schedule的时候,先获取当前系统时间,在这个基础上,加上(延时执行),得到了真实要执行这个任务的时间
2.使用一定的数据结构,把多个TimerTask组织起来
思路1(看看行不行):咱们用(链表/数组)组织TimerTask,如果任务特别多,如何确定哪个任务,何时能够执行呢?这时候就需要搞一个线程,不断的去遍历List,进行遍历,看着里面的每个元素,是否到了时间,时间到就执行,不到就跳过下一个。
回答:不行,这个思路可以,但是非常不好,假如任务执行时间都特别长,你这段时间一直都在遍历,扫描,会很消耗cpu(还没做事)。
思路2(正解):
1.并不需要扫描全部的任务,我们其实只需要关注一个,就是最早,时间最靠前的任务即可,最前面的任务假如还没到达的话,其他的任务更不会到达!!!
遍历一个->只关注一个,那么也就是说最好能选出最大,最小这种的数据结构(脑子里第一个肯定就是——堆(优先级队列))。
2.针对这个任务扫描,也不用一遍一遍一直重复执行,我们获取完队首时间(最小的任务时间)和当前时间做差值,在这个差值到达之前,可以让他休眠或者等待,这样不会进行重复的扫描,大幅度降低了扫描的次数->这样也不消耗cpu,(此处提高效率,不是缩短执行时间(定时器,时间是固定的),减少了资源利用率,避免了不必要的浪费)
战损没有改良的简版。
class MyTimertask{ private long time; //任务什么时候执行,毫秒级的时间戳 private Runnable runnable; //具体的任务 public MyTimertask(Runnable runnable,long delay){ time=System.currentTimeMillis()+delay; this.runnable=runnable; } public long getTime() { return time; } public Runnable getRunnable() { return runnable; } } class MyTimer { //使用优先级队列 private PriorityQueue<MyTimertask> queue = new PriorityQueue<>(); //定时器核心方法 public void schedule(Runnable runnable, long delay) { MyTimertask task = new MyTimertask(runnable, delay); queue.offer(task); } //定时器还要搞个扫描方法,内部的线程。一方面负责监控队首元素是否到点了,是否应该执行, // 一方面当任务到了之后,就要调用这里的Runable的run方法执行任务 public MyTimer() { Thread t = new Thread(() -> { while (true) { //队列为空,此时不可以取元素 if (queue.isEmpty()) { continue; } MyTimertask Task = queue.peek(); long curTime = System.currentTimeMillis(); if (curTime >= Task.getTime()) { queue.poll(); Task.getRunnable().run(); } else { try { Thread.sleep(Task.getTime() - curTime); } catch (InterruptedException e) { e.printStackTrace(); } } } }); t.start(); } }
当前战损版的问题:
1.线程不安全,PriorityQueue<MyTimeTask> queue=new PriorityQueue<>();
这个集合类不是线程安全的,他这个既在线程中使用,又在扫描线程中使用,给针对queue的操作进行加锁。(直接无脑暴力加锁)
2.扫描线程中的sleep是否合理呢?
(1)sleep阻塞后,不会释放锁(休眠的时候,最短的时间都还很长,这么大段时间无法添加任务)影响其他线程执行添加
(2)slepp在休眠过程中,不方便提前中断,(虽然可以用interrupt来中断,单用interrupt意味线程应该结束了)
假设当前最近的任务是14:00执行,当前时刻是13:00(sleep一小时),此时新增一个新的任务,新任务13:30要执行(成为了最早的任务),所以为了中断,应该使用wait
(3)我们设置的时候,需要把可比较写上,也就是说Comparable/Comparaor,也要知道,是什么需要比较呢,时间(顺序记不清无所谓,试一下就OK了)。
import java.util.PriorityQueue; //创建一个类,用来描述定时器中的一个任务 class MyTimertask implements Comparable<MyTimertask>{ private long time; //任务什么时候执行,毫秒级的时间戳 private Runnable runnable; //具体的任务 public MyTimertask(Runnable runnable,long delay){ time=System.currentTimeMillis()+delay; this.runnable=runnable; } public long getTime() { return time; } public Runnable getRunnable() { return runnable; } @Override public int compareTo(MyTimertask o) { return (int)(this.time-o.time); } } class MyTimer { //使用优先级队列 private PriorityQueue<MyTimertask> queue = new PriorityQueue<>(); //定时器核心方法 public void schedule(Runnable runnable, long delay) { synchronized (this) { MyTimertask task = new MyTimertask(runnable, delay); queue.offer(task); //新来一个任务,就需要把线程唤醒 this.notify(); } } //定时器还要搞个扫描方法,内部的线程。一方面负责监控队首元素是否到点了,是否应该执行, // 一方面当任务到了之后,就要调用这里的Runable的run方法执行任务 public MyTimer() { Thread t = new Thread(() -> { while (true) { synchronized (this) { //队列为空,此时不可以取元素 while (queue.isEmpty()) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } MyTimertask Task = queue.peek(); //任务的时间 long curTime = System.currentTimeMillis(); //当前的系统时间 if (curTime >= Task.getTime()) { //系统时间 queue.poll(); 14:01>任务时间14:00 ——执行任务 Task.getRunnable().run(); } else { try { this.wait(Task.getTime() - curTime); } catch (InterruptedException e) { e.printStackTrace(); } } } } }); t.start(); } } public class Demo3 { public static void main(String[] args) { MyTimer myTimer = new MyTimer(); myTimer.schedule(new Runnable() { @Override public void run() { System.out.println("hello"); } }, 3000); myTimer.schedule(new Runnable() { @Override public void run() { System.out.println("god"); } }, 2000); myTimer.schedule(new Runnable() { @Override public void run() { System.out.println("good"); } }, 1000); } }
这应该是自从数据结构以来,我们学过最难的了,所以大家多敲几遍理解理解