多线程案例(3) - 定时器,线程池

一,定时器

定时器作用:约定一个时间间隔,时间到达后,执行某段代码逻辑。实际上就是一个 "闹钟" 。

1.1使用标准库中的定时器

  • 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
  • Timer 类中含有一个扫描线程,观察是否有任务到达执行时间
  • schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒)
  • TimerTask 类继承了 Runnable 接口,所以能重写 run() 方法 
public class Test {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("1111");
            }
        },1000);
    }
}

这是因为 Timer 内部的线程阻止了 进程 的结束,在 Timer 中是可以安排多个任务的,我们下面实现的时候要注意这一点。

1.2 定时器的实现

1. Timer 中需要一个扫描线程,来扫描任务是否到时间,可否执行。

2. Timer 可以安排多个任务执行,而每个任务的执行时间又不一样,所以我们需要使用优先级队列来存储任务,让这些任务按时间顺序排列。

3. 还需要创建一个类,通过类来描述一个任务,(包含任务的内容和时间)

class MyTimer{
    private PriorityQueue<MyTimerTask> priorityQueue = new PriorityQueue<>();

    public MyTimer(){
        //扫描线程
        Thread t = new Thread(() -> {
            while(true){
                synchronized (this){//涉及到修改操作,加锁
                    try{
                        while(priorityQueue.isEmpty()){
                            this.wait();
                        }

                        MyTimerTask myTimerTask = priorityQueue.peek();
                        long curTime = System.currentTimeMillis();//得到当前时间

                        if(curTime >= myTimerTask.getTime()){//到达执行时间
                            myTimerTask.getRunnable().run();
                            priorityQueue.poll();
                        }else {//未到达执行时间
                            this.wait(myTimerTask.getTime() - curTime);//线程等待
                            //如果没有这句代码,就会出现忙等,类似于,一直在看表
                        }
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();
    }

    public void schedule(Runnable runnable, long delay){
        synchronized (this){
            MyTimerTask myTimerTask = new MyTimerTask(runnable,delay);
            priorityQueue.offer(myTimerTask);//将任务放入队列
            this.notify();//如果当前队列为空,唤醒线程
        }
    }
}

class MyTimerTask implements Comparable<MyTimerTask>{
    private Runnable runnable;//任务内容
    private long time;//任务执行的具体时间

    public MyTimerTask(Runnable runnable, long delay){
        this.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);
    }
}

关于上述代码还有几个细节需要注意:

1. 

因为 wait() 也可能会被InterruptedException打断,如果使用 if ,这时候队列仍然为null,就不能出现错误。

2.

因为如果使用sleep,有一种场景是不能成立的,就是当我们插入一个执行时间更早的任务时,线程还是处于休眠状态,这时候新插入的任务就会延迟执行,这不符合我们的逻辑。

而使用 wait 的话,线程就会重新去找那个最先执行的任务。

二,线程池

线程池能够减少线程创建和销毁的开销,也就是说适用于线程频繁创建销毁的场景。

2.1 使用标准库中的线程池

Executors 创建线程池的几种方式

  • newFixedThreadPool: 创建固定线程数的线程池
  • newCachedThreadPool: 创建线程数目动态增长的线程池,线程执行完后,不会立即销毁,而是会缓存一段时间
  • newSingleThreadExecutor: 创建只包含单个线程的线程池.
  • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
public class Demo {
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        ExecutorService service1 = Executors.newFixedThreadPool(3);
        ExecutorService service2 = Executors.newSingleThreadExecutor();
        ExecutorService service3 = Executors.newScheduledThreadPool(2);

        service.submit(new Runnable() {//通过 ExecutorService.submit 可以注册一个任务到线程池
            @Override
            public void run() {
                System.out.println("111");
            }
        });
    }
}

这里为什么不使用 new 的方法来创建一个线程池,而是使用 "工厂模式" 来实现呢?

首先了解一下什么是工厂模式,工厂模式是指使用普通方法来代替构造方法完成初始化工作,因为普通方法可以通过方法名来区分,也就不用受到重载规则的限制。比如:我们的坐标既可以使用笛卡尔坐标系,也可以使用极坐标系,这两个构造方法的参数是完全相同的,这时候我们就要使用 "工厂模式" 来初始化。

Executors 本质上是 ThreadPoolExecutor 类的封装,ThreadPoolExecutor 提供了更多的可选参数, 可以进一步细化线程池行为的设定。下面我们来介绍一下构造方法的参数(很重要!!!)。

  • corePoolSize : 线程池最少有多少线程
  • maximumPoolSize : 线程池最多有多少线程
  • keepAliverTime : 线程有多长的 "摸鱼" 时间,如果一个线程有 keepAliverTime 个时间没有工作,那么就会销毁该线程。
  • unit : keepAliverTime 的单位
  • workQueue :阻塞队列,如果需要优先级,就设置 PriorityBlockingQueue,如果有数量限制,就设置 ArrayBlockingQueue,如果数目变动较大,就设置 LinkedBlockingQueue
  • threadFactory :工厂模式,使用工厂模式来创建线程,设置一些线程的属性
  • handler :线程池的拒绝策略,一个线程池容纳的任务数量是有上限的,当到达上限后,继续添加线程的处理方式,处理的4种方式如下:

 这里还有一道经典的面试题:如果使用线程池需要设定线程的数目,设置成多少合适?

这个时候,只要回答出具体的数字就错的,因为一个线程执行的代码有两类:

1)cpu 密集型:代码主要进行算术运算 / 逻辑运算

2)IO 密集型:代码主要进行 IO 操作

假设一个线程的所有代码都是 cpu 密集型,这时候线程池的线程数量不应该超过 N (cpu 逻辑核心数),大于 N,也无法提高效率。

假设一个线程的所有代码都是 IO 密集型,这时候不吃 cpu,这时候就可以超过 N.

代码不同,一个线程池的线程数量设置就不同,因为我们无法知道一段代码,有多少是cpu密集型,多少是 IO 密集型。正确的回答是:使用实验的方式,对程序进行性能测试,在测试过程中不段的调整线程池的线程数量,看哪种情况更符合要求。

 2.2 线程池的简单实现

import java.util.concurrent.*;

class MyThreadPool{
    BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(4);

    public void submit(Runnable runnable){
        try {
            blockingQueue.put(runnable);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    public MyThreadPool(int n){
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(()->{
                try {
                    Runnable a = blockingQueue.take();
                    a.run();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
    }
}

猜你喜欢

转载自blog.csdn.net/m0_74859835/article/details/132918361