先实现一下普通队列,看看运行结果咋样
class MyBlockingQueue{
//使用一个String类型的数组来保存元素,假设这里只存String
private String[] items = new String[1000];
//指向队列的头部
private int head = 0;
//指向队列的尾部的下一个元素,总的来说,队列的有效元素的范围[head,tail)
//当head和tail相等(重合)的时候,相当于空的队列
private int tail = 0;
//我们把这个数组想象成一个圆环,每增加一个元素,tail就++到下一个位置,所以当最后一个位置填满,head和tail又重合了
//所以我们无法无法判断head和tail重合是空还是满
//有两个办法(1):浪费一个位置,当head走到head前一个位置就认为是满了.(2)单独搞一个变量,表示元素个数
//使用size来表示元素个数
private 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;
}
}
public class Demo20 {
public static void main(String[] args) {
MyBlockingQueue queue = new MyBlockingQueue();
queue.put("aaa");
queue.put("bbb");
queue.put("ccc");
String elem = queue.take();
System.out.println("elem="+elem);
elem = queue.take();
System.out.println("elem="+elem);
elem = queue.take();
System.out.println("elem="+elem);
elem = queue.take();
System.out.println("elem="+elem);
}
}
运行起来没问题,现在我们将其改造成阻塞队列
1.首先我们要先解决线程安全问题,先给put和take里面的代码全部加锁,先保证在多线程调用的时候,能够保证线程安全,除了加锁之外,还要考虑内存可见性问题,把head,tail,size,都加上volatile
2.实现阻塞,当队列满的时候就会出现阻塞,用wait和notify,在put的代码中,如果队列满了就wait,然后在出队列代码中,出掉一个信息,就可以notify唤醒wait,通知它可以继续添加新的元素了
当队列为空的时候,再进行take也会产生阻塞,所以在take代码中,如果队列为空就wait,直到入队列增加了一个信息,就notify这个wait
此处的两个wait并不会同时出现,因为咱这个队列不可能既空又满
这里还有一个问题,假如此时队列满了,wait阻塞了,但是万一是interrupt唤醒的wait,可能此时的队列还是满的,被唤醒以后继续执行,就会有可能把之前存入的元素给覆盖了,在我们现在的代码中,如果是interrupt唤醒了,此时会直接引起异常,方法就结束了,就不会出现覆盖已有元素的问题.但是如果我们是按照try catch的方式来写,一旦是interrupt唤醒,此时代码继续往下走进入catch,catch执行完毕,方法不会结束,继续往下执行,也就会出发"覆盖元素"逻辑
要想万无一失解决这个问题,我们可以把if改成while,被唤醒以后再次判断是否是满的,直到真的判断成功才会跳出循环继续向下执行
完整代码如下
class MyBlockingQueue{
//使用一个String类型的数组来保存元素,假设这里只存String
private String[] items = new String[1000];
//指向队列的头部
volatile private int head = 0;
//指向队列的尾部的下一个元素,总的来说,队列的有效元素的范围[head,tail)
//当head和tail相等(重合)的时候,相当于空的队列
volatile private int tail = 0;
//我们把这个数组想象成一个圆环,每增加一个元素,tail就++到下一个位置,所以当最后一个位置填满,head和tail又重合了
//所以我们无法无法判断head和tail重合是空还是满
//有两个办法(1):浪费一个位置,当head走到head前一个位置就认为是满了.(2)单独搞一个变量,表示元素个数
//使用size来表示元素个数
volatile private int size=0;
private Object locker= new Object();
//入队列
public void put(String elem) throws InterruptedException {
synchronized (locker){
while (size >= items.length) {
//队列满了
//return;
locker.wait();
}
items[tail] = elem;
tail++;
if (tail >= items.length) {
tail = 0;
}
size++;
locker.notify();//用来唤醒队列为空的阻塞状态
}
}
public String take() throws InterruptedException {
synchronized (locker) {
while (size == 0) {
//队列为空,暂时不能出队列
//return null;
locker.wait();
}
String elem = items[head];
head++;
if (head >= items.length) {
head = 0;
}
size--;
locker.notify();
return elem;
}
}
}
public class Demo20 {
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue queue = new MyBlockingQueue();
queue.put("aaa");
queue.put("bbb");
queue.put("ccc");
String elem = queue.take();
System.out.println("elem="+elem);
elem = queue.take();
System.out.println("elem="+elem);
elem = queue.take();
System.out.println("elem="+elem);
elem = queue.take();
System.out.println("elem="+elem);
}
}
接下来我们用自制的阻塞队列实现生产者消费者模型
public class Demo20 {
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue queue = new MyBlockingQueue();
Thread t1 = new Thread(()->{
int count = 0;
while (true){
try {
queue.put(count+"");
System.out.println("生产元素:"+count);
count++;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2=new Thread(()->{
while (true){
try {
String count=queue.take();
System.out.println("消费元素:"+count);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
下一块内容,实现定时器,这个是日常开发中常见的组件,前端后端都会用到定时器,类似闹钟
类似于Runnable,TimerTask是抽象类,实现Runnable接口
我们给它分配一个打印hello的任务
import java.util.Timer;
import java.util.TimerTask;
public class Demo21 {
public static void main(String[] args) {
Timer timer = new Timer();
//给timer中安排的这个任务,不是在调用schedule的线程中执行的,而是通过Timer内部的线程来负责执行都得
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
},3000);
}
}
运行结果就是三秒过后,打印出hello,但是打印完以后进程没有结束,这是因为Timer内部有自己的线程,为了保证随时可以处理新安排的任务,这个线程会持续执行,并且这个线程还是个前台线程
接下来我们自己实现定时器
先要能够把一个任务给描述出来,再使用数据结构把多个任务组织起来
定时器是可以安排多个任务的,想弄几个就弄几个timer.schedule就行
1.创建一个TimerTask这样的类,表示一个任务,这个任务需要包含两个方面,任务的内容和任务的实际执行时间,执行时间我们可以用时间戳表示,在schedule的时候,先获取到当前的系统时间,在这个基础上,加上delay时间间隔,得到了真实要执行这个任务的时间
2.使用一定的数据结构,把多个TimerTask给组织起来 如果使用List(数组,链表)组织TimerTask的话,如果任务特别多,如何确定哪个任务,何时能够执行呢?这样就需要搞一个线程,不停地对上述的List进行遍历,看看这里的每一个元素,是否到了时间,时间到就执行,时间没到就换下一个,这个思路是不科学的,如果这些任务的执行时间都为时尚早,那在时间到达之前,这个扫描过程就需要一刻不停地重复
我们只需要盯住最靠前的任务即可,最早的任务没到,其他的更不会到,所以我们可以用优先级队列来组织所有任务,队首就是时间最小的任务,我们获取到队首元素的时间之后,和当前的系统时间做个差值,根据这个差值,来决定休眠/等待的时间,在这个时间到达之前,不会进行重复扫描,降低了扫描的次数,休眠不会消耗CPU资源
实现定时器的代码如下
import java.util.PriorityQueue;
import java.util.Timer;
//创建一个类,用来描述定时器中的一个任务
class MyTimerTask implements Comparable{
//任务啥时候执行,毫秒级的时间戳
private long time;
//任务具体是啥
private Runnable runnable;
//构造方法
public MyTimerTask(Runnable runnable,long delay){
//delay 是一个相对的时间差,例如3000 这样的数值
//构造time 要根据当前系统时间和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{
//使用优先级队列,来保存上述的N个任务
private PriorityQueue queue = new PriorityQueue<>();
//用来加锁的对象
private Object locker = new Object();
//定时器的核心方法,就是把要执行的任务添加到队列中
public void schedule(Runnable runnable,long delay){
synchronized (locker) {
MyTimerTask task = new MyTimerTask(runnable, delay);
queue.offer(task);
//每次来新的任务都唤醒扫描线程,好让扫描线程根据最新的情况重新安排等待时间
locker.notify();
}
}
//MyTimer中还需要构造一个"扫描线程",一方面去负责监控首元素是否到点了,是否应该执行
//一方面当任务到点之后,就要调用这里的Runnable方法中的Run方法来完成任务
public MyTimer(){
//扫描线程
Thread t = new Thread(()->{
while(true){
try{
synchronized (locker) {
while (queue.isEmpty()) {
locker.wait();
}
MyTimerTask task = queue.peek();
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()) {
//假设当前时间是14.01,任务时间是14.00,此时就意味着要执行这个任务
queue.poll();
task.getRunnable().run();
} else {
//让当前扫描线程休眠一下,按照时间差来进行休眠
//Thread.sleep(task.getTime() - curTime);
locker.wait(task.getTime()-curTime);
}
}
} catch(InterruptedException e){
e.printStackTrace();
}
}
});
t.start();
}
}
public class Demo22 {
public static void main(String[] args) {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 3");
}
},3000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 2");
}
},2000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 1");
}
},1000);
System.out.println("程序开始运行");
}
}
以下是容易忽视的三个问题