自己实现阻塞队列和定时器

先实现一下普通队列,看看运行结果咋样

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张图片

 运行起来没问题,现在我们将其改造成阻塞队列

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();
    }
}

运行结果如图,成功运行没毛病自己实现阻塞队列和定时器_第2张图片

 

下一块内容,实现定时器,这个是日常开发中常见的组件,前端后端都会用到定时器,类似闹钟

自己实现阻塞队列和定时器_第3张图片

 类似于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("程序开始运行");
    }
}

自己实现阻塞队列和定时器_第4张图片

以下是容易忽视的三个问题

 自己实现阻塞队列和定时器_第5张图片

 自己实现阻塞队列和定时器_第6张图片

 自己实现阻塞队列和定时器_第7张图片

 

你可能感兴趣的:(java,算法,开发语言)