【多线程】定时器 | 线程池 | 实现MyTimer | 实现MyThreadPoll | 工厂模式 | 构造方法 | 参数种类

定时器&线程池


一、定时器

  • 类似一个"闹钟",约定一个时间,时间到达之后,执行某个代码逻辑
  • 在进行网络通信中很常见

客户端在发出请求后,就要等待响应。但是如果服务器迟迟没有响应,客户端不能无限的等下去,需要有一个最大的期限,等时间到了之后再次进行判断。而“等待的最大时间”就可以通过定时器的方式来实现。

1.标准库中的定时器
  • import java.util.Timer 。 Timer这个类是在util里的
    public static void main(String[] args) {
        Timer timer = new Timer();
        //给定时器安排了一个任务,预订在2秒后执行。
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行定时器");
            }
        },2000);
        System.out.println("程序启动");
    }

程序启动
执行定时器//两秒后
  • schedule()方法的第一个参数:使用匿名内部类,创建一个TimerTask()实例,重写当中的run方法,通过run方法来描述任务的详细情况

在这里插入图片描述

TimerTask类本身就实现了Runnable接口。从而重写run方法

  • schedule方法的第二个参数:填写的时间,表示当前这个任务以此时此刻为基准,往后推X时间后再执行该任务。

    当主线程在执行schedule方法的时候,就会把任务放进timer对象中。同时,timer当中,存在一个扫描线程。一旦时间到了,扫描线程就会执行刚才安排的任务。换句话说,timer当中的任务,是有当中的扫描线程来执行的。当任务结束时,扫描线程并未结束,还在等待执行后续可能安排的任务

    public static void main(String[] args) {
        Timer timer = new Timer();
        //给定时器安排了一个任务,预订在2秒后执行。
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行定时器2");
            }
        },2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行定时器3");
            }
        },3000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行定时器1");
            }
        },1000);

        System.out.println("程序启动");
    }

程序启动
执行定时器1
执行定时器2
执行定时器3
2.实现定时器

要有一个扫描线程,扫描任务是否到达时间执行

要有一个优先级对列来保存任务:o(1),优先取时间最小的任务执行。

创建一个类,通过类的对象来描述任务(任务内容、任务时间)

class MyTimerTask implements Comparable<MyTimerTask> {
    //描述一个任务
    private Runnable runnable;
    //要执行的任务

    private long time;

    MyTimerTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;//当前的时间戳+要延迟的时间
    }

    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        //保证队首的任务是是最小的时间
        return (int) (this.time - o.time);
    }
}

  • 如果队列为空了,就需要 调用wait来进行阻塞,同时需要搭配synchronized来使用。因为wait的操作有三个:1在前提有锁的情况下,释放锁。2.等待通知。3.通知到来之后,进行唤醒,同时重新拿到锁。
  • 对应的,也需要在调用schedule方法添加任务时,对之前因为队列为空而等待的wait进行唤醒(notify)

同时:由于schedule方法和扫描线程都会操作队列。存在线程安全问题。因此要加锁。

//自己实现的定时器
class MyTimer {
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    private Object locker = new Object();
    //锁对象

    //优先级队列存任务
    public void schedule(Runnable runnable, long delay) {
        //把要完成的任务和延迟的时间构造成一个任务对象,存进优先级队列
        synchronized (locker) {
            queue.offer(new MyTimerTask(runnable, delay));
            locker.notify();//唤醒空对列的wait
        }
    }

    public MyTimer() {
        //创建一个扫描线程
        Thread t = new Thread(() -> {
            while (true) {
                //不停扫描队首元素
                try {
                    synchronized (locker) {
                        while (queue.isEmpty()) {
                            //使用wait进行等待
                            locker.wait();//需要由添加任务的时候唤醒
                        }
                        MyTimerTask task = queue.peek();
                        //比较一下当前时间是否可以执行任务
                        long curTime = System.currentTimeMillis();
                        if (curTime >= task.getTime()) {
                            task.getRunnable().run();//执行任务
                            queue.poll();
                        }else {
                            locker.wait(task.getTime()-curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start(); 
    }
}

​ 由于扫描线程是while(true)循环,在队列不为空的情况下,不会进入等待。会一直持续循环,直到当前时间到达设定的时间为止。在此期间并没有进行任何操作,只是不断的对表忙等,但是消耗了很多cpu资源。所以在第一次判断时间时,在else中,当任务时间还没到的时候,进行wait阻塞,此时线程不会在CPU上调度,避免了忙等。

  • 同时:如果schedule方法添加了一个比当前待任务要早执行的任务时,schedule方法内部的notify就会唤醒这个带参数的wait,让循环再执行一次,重新拿到新的队首元素,跟新wait的时间。

也就是说:当队列为空时进行阻塞等待,调用一次schedule方法,其中的notify可以唤醒wait。而在等待要执行的任务时,调用schedule方法存入一个任务。其中的notify会唤醒带参数的wait,再次循环,重新获取队首任务,更新等待时间。

二、线程池

1.线程池的概念

​ 由于进程创建/销毁,太重量了(比较慢)才引进了线程的概念,但是如果进一步提高创建销毁的频率,线程的开销就不容忽视。

有两种办法来提高线程的效率:

  • 1.协程(轻量级线程)

    线程省略的是资源创建的过程。先比与线程,协程省略了系统调度的过程。(程序员手动调度)

    Java虽然标准库中没有协程,但是一些第三方库实现了协程。

  • 2.线程池 同样可以提高线程的效率,同时避免了全面的修改。减少了每次创建、销毁线程的损耗

线程池:

​ 在使用第一个线程的时候,提前把后续多个线程创建好,放进池中。后续如果想使用多个线程,不必重新创建,直接从池中拿过来用,降低创建线程的开销。

而从池中这个操作,是用户态的操作。而创建一个线程,则是用户态+内核态 相互配合来完成的操作。

​ 如果一段程序在系统内核中执行,就被称为内核态。否则就是用户态。而一个操作系统则是由操作系统内核和配套的应用程序构成。创建一个线程,就会需要调用系统API,进入到内核中,按照内核态的方式来完成一系列操作。操作系统内核是要对所有的进程提供服务的,当要创建一个线程的时候,内核难免会被干扰做其他事情,不可控,不可预期。而用户态的操作不涉及系统内核,可控可预期。因此线程池的操作要比创建线程更高效。

2.标准库当中的线程池
        ExecutorService service = Executors.newCachedThreadPool()
         //可执行的服务
  • 线程池对象不是直接new出来的,而是通过专门的方法,返回了一个线程池对象。这种写法叫做工厂模式
工厂模式

工厂模式是一种常见的设计模式

​ 通常在使用new关键字创建对象时,会触发类的构造方法来实例对象。但是构造方法存在一定的局限性,工厂模式就可以解决构造方法的不足

class Point{
    public Point(double x,double y){//通过笛卡尔坐标系构造点
    }
    public Point(double r,double a){//使用极坐标构造点
    }
}
  • 此时,在这个类中,两个构造方法采用的是两种截然不同的方式,但是构造方法的方法名必须是类名,不同的构造方法只能通过重载来进行区分(重载要求参数类型/个数不同)。但是此时,两个构造方法的参数列表相同,没有完成重载,编译失败。
  • 而采用工程模式,使用普通的方法,代替构造方法来完成初始化工作,普通方法就可以使用不同方法名来进行区分。
class PointFactory{
    public static Point makePointByXY(double x,double y){
        Point p  =new Point();
        p.setX(x){}
        p.setY(y){}
      return p;
    }
    public static Point makePointByRA(double x,double y){
        return p;
    }
}

Point p = PointFactory.makePointByXY(15,20);
//就类似于线程池的创建
ExecutorService service = Executors.newCachedThreadPool()
                            //工厂类   //工厂方法
  • 通过方法名来完成区分
Executors 创建线程池
1.自适应线程池

​ newCachedThreadPool,可以动态适应。随着往线程池中添加任务,这个线程池中的线程会根据需要自动被创建出来,并且使用后不会立即销毁,会在池中保留一定的时间,以备后续再次使用

        ExecutorService service = Executors.newCachedThreadPool();//可以动态适应
			//cached:缓存,用过之后不着急释放,先保留,
2.固定数量线程池
		 ExecutorService service1 = Executors.newFixedThreadPool(10);//固定的
3.只有单个线程的线程池
		ExecutorService service2 = Executors.newSingleThreadExecutor();
4.设定延迟时间后执行命令的线程池
        ExecutorService service3 = Executors.newScheduledThreadPool(5); 
//类似于定时器,但是不在是一个扫面线程在执行任务,而是变成了多个线程来执行任务。
ThreadPoolExecutor 类

​ Executors 本质上是 ThreadPoolExecutor 类的封装。ThreadPoolExecutor 类的功能非常丰富,提供了很多参数。标准库当中的几个工厂方法,其实就是给这个类填写了不同的参数来构造线程池

ThreadPoolExecutor 类的核心方法有两个:

1.注册任务
        ExecutorService service1 = Executors.newFixedThreadPool(10);
        service1.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
2.构造

ThreadPoolExecutor当中的构造参数有很多(面试题)

在这里插入图片描述

JUC这个包就是和并发编程相关的内容(多线程)

ThreadPoolExecutor的构造方法有四个版本,其中最后一个版本参数最多,可以涵盖其余方法的参数

在这里插入图片描述

​ int corePoolSize(核心线程数)int maximumPoolSize(最大线程数), 描述线程的数目

这个线程池中,线程的数目是可以动态变化的,线程数变化的范围就是 [ corePoolSize, maximumPoolSize ]

核心线程数(正式员工数量) ;最大线程数(正式员工数量 + 实习生的数量)

实习生不允许摸鱼,活多了招人,少了裁人。但是不会动正式员工

在满足效率的同时,又可以避免过多的系统开销

​ BlockingQueue 阻塞队列:可以根据需要灵活选择队列,需要有优先级,设置PriorityBlockingQueue

如果不需要优先级,并且任务数量相对恒定,使用ArrayBlockingQueue。如果任务数量变动较大,

使用LinkedBlockingQueue.

​ 使用工厂类来创建线程,主要是为了在创建的过程中,对线程的属性做一些设置。如果手动创建线程,就需要手动设置这些属性,所以用工厂方法进行封装。

​ RejectedExecutionHandler handler 线程池的拒绝策略,一个线程池的容量是有限的,达到上限后,采用不同的拒绝策略会有不同的效果。(4种)

在这里插入图片描述

使用线程池,需要设置线程的数目

设置多少线程合适?

在接触到实际的项目代码之前,是无法确定的。

一个线程,要执行的代码主要有两大类:

1.CPU密集型:代码中主要的逻辑是进行算数运算/逻辑判断

2.IO密集型:代码里主要进行IO操作。(网络通信、写硬盘、读硬盘)

​ 假设一个线程的所有代码都是CPU密集型代码,线程池中的线程数量不应该超过N(CPU核心数),设置的比N大,cpu吃满了,无法提高效率,此时添加更多的线程反而增加调度的开销。

​ 假设一个线程的所有代码都是IO密集型的,这个时候不吃CPU,此时设置的线程数,就可以超过N.一个核心可以通过调度的方式,来并发执行。

​ 代码不同,线程池的线程数目设置就不同。正确的设置方法:使用实验的方式,对程序进行性能测试。在测试的过程中,尝试修改不同的线程池的线程数目。看哪种情况最符合需求。

3.实现线程池
class MyThreadPool{
    //任务队列
    private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);

    //通过submit方法,将任务添加到队列中
    public void submit(Runnable runnable) throws InterruptedException {
        //次处的拒绝策略相当于第5种策略:阻塞等待
        queue.put(runnable);
    }

    public MyThreadPool(int n){
        //创建出n个线程,负责执行上述队列中的任务
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(()->{
                //让线程,从队列中消费任务,并进行执行
                try {
                    Runnable runnable = queue.take();
                    runnable.run();
                } catch (InterruptedException e) {
                   e.printStackTrace();
                }
            });
            t.start();
        }
    }
}
public class MakeMyThreadPoll {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(4);
        for (int i = 0; i < 1000; i++) {
            int id = i;
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行任务 " + id);
                    //防止匿名内部类的变量捕获
                    //此时捕获的是id,id没有人进行修改。每次循环都创建了新的id
                }
            });
        }
    }

点击移步博客主页,欢迎光临~

偷cyk的图

相关推荐

  1. 线参数 以及实现

    2024-04-22 00:32:01       17 阅读
  2. 简易线实现

    2024-04-22 00:32:01       19 阅读
  3. 线种类有哪些

    2024-04-22 00:32:01       18 阅读
  4. Go 通过 goroutines 实现类似线模式

    2024-04-22 00:32:01       29 阅读
  5. 线实例

    2024-04-22 00:32:01       7 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-22 00:32:01       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-22 00:32:01       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-22 00:32:01       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-22 00:32:01       20 阅读

热门阅读

  1. 深入浅出理解CSS中的3D变换:踏上立体视觉之旅

    2024-04-22 00:32:01       16 阅读
  2. k8s中修复mongodb启动失败

    2024-04-22 00:32:01       12 阅读
  3. Neural Radiance Fields (NeRF) 和 3D Gaussian Splatting区别

    2024-04-22 00:32:01       16 阅读
  4. 展开说说:Android Fragment完全解析-卷二

    2024-04-22 00:32:01       16 阅读
  5. vue大屏

    2024-04-22 00:32:01       15 阅读
  6. jQuery 选择器有几种,分别是什么

    2024-04-22 00:32:01       14 阅读
  7. Linux系统的账号和权限管理

    2024-04-22 00:32:01       15 阅读
  8. 赠品:跳动的心

    2024-04-22 00:32:01       12 阅读
  9. ZYNQ-700呼吸灯

    2024-04-22 00:32:01       13 阅读
  10. 【设计模式】8、adapter 适配器模式

    2024-04-22 00:32:01       12 阅读
  11. 考古:MFC界面的自适应缩放(代码示例)

    2024-04-22 00:32:01       13 阅读