前言 

        逆水行舟,不进则退!!!     


       目录

       什么是定时器      

       实现一个定时器 

自己实现一个定时器

       什么是线程池 

        线程池的使用:

什么是工厂模式?

自己实现一个线程池:

       ThreadPoolExecutor 类

什么是Runnable 任务?

什么是 Callable 任务?

获取异步的执行结果 是什么意思?

ThreadPoolExecutor类的构造方法有7个参数,


       什么是定时器      

        在Java编程中,定时器是一种工具,它用于在指定的时间点主动触发某个事件,而无需外力去开启或启动。这种机制可以节省人力并实现统一管理。 其中,java.util.Timer类是最常用的定时器实现方式,它允许开发者安排在指定时间运行的任务。 使用Timer类创建定时器主要包括以下步骤:

        首先,创建一个Timer对象;

        其次,创建一个TimerTask对象,该对象包含了要执行的代码;

        然后,将TimerTask对象添加到Timer对象中;

        最后,调用Timer对象的schedule方法来安排任务的执行。

         此外,定时计划任务功能在Java中主要使用的就是Timer对象,它在内部使用多线程的方式进行处理,所以Timer对象一般又和多线程技术结合紧密。

        定时器的使用:

import java.util.Timer;
import java.util.TimerTask;

public class ThreadDemo10 {
    public static void main(String[] args) {
        System.out.println("程序启动");
        // Timer 类就是 标准库的定时器
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {   //TimerTask是timer.schedule方法的第一个参数,
                                // 其实就是Runnable,TimerTask就是一个实现了Runnable的抽象类。
                                // 也要通过run() 方法来描述一段代码。
            @Override
            public void run() {
               
                System.out.println("运行定时器任务3");
            }
        }, 3000);  // TimerTask 的第二个参数是,指定的时间,
                        //  意思就是,一段时间后,触发第一个参数描述的代码。

        
        // 多写两个,感受一下定时执行的任务
        timer.schedule(new TimerTask() {   
            @Override
            public void run() {
                System.out.println("运行定时器任务2");
            }
        }, 2000);  



        timer.schedule(new TimerTask() {   

            @Override
            public void run() {
                System.out.println("运行定时器任务1");
            }
        }, 1000); 


    }
}

       实现一个定时器 
自己实现一个定时器

        分析:
               1,让被注册的任务,能够在指定时间被执行。
               2,一个定时器是可以注册 N 个任务的,N 个任务会按照最初约定的时间,按顺序执行。
               若要实现 1, 就需要在定时器内部,单独弄一个线程,让这个线程周期性的扫描,判断任务是否是到时间了。如果到时间了,就执行,没到时间,就再等等。并且 N 个任务也需要保存。
 
 所以,定时器中的核心:
         1, 有一个扫描线程,负责查看时间到没到,到了就执行相应任务
         2, 还要有一个数据结构,来保存所有被注册的任务。
         选用什么数据结构呢:  每个任务都是带着“时间”的, 所以我们这里选用优先级队列来存储。 时间段小的,优先级就高, 此时的扫描线程只用扫描队首元素即可,不必遍历整个队列。

     此处的优先级队列是要在 多线程  环境下使用,要考虑线程安全问题。




import java.util.concurrent.PriorityBlockingQueue;

//开始写 定时器
class MyTimer {
    //扫描线程
    private Thread t = null;

    // 有一个阻塞优先级队列, 来保存任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    //扫描线程的具体实现
    public MyTimer() {
        t = new Thread(() -> {
            while(true) {

                try {
                    //取出队首元素,检查看看队首元素任务是否到时间了,
                    //如果时间没到,就把任务再塞回队列中
                    //如果时间到了,就执行任务。
                    MyTask myTask = queue.take();   // 将任务从 阻塞优先级队列中拿出来。
                    synchronized (this) {          // 这里使用 wait   主要是为了防止 忙等。
                        //  这个 synchronized 本来是放到 wait 那里,
                        //  放到这里是为了 保证 取出任务 和 wait  原子化, 防止在中间线程被调度走而且同时来了新任务。
                        long curTime = System.currentTimeMillis();  //获取当前的时间
                        if (curTime < myTask.getTime()) {
                            //说明当前的时间 还没到要执行任务的时间
                            queue.put(myTask);   // 再把任务 放回到阻塞优先级队列中。


                            //在 put 之后, 进行wait 等待
                            // 等待指定时间
                            this.wait(myTask.getTime() - curTime);  // wait 操作,要搭配 锁 来进行的

                        } else {
                            // 已经到了要执行任务的时间了, 可以开始执行任务了。
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }


            }
        });
        t.start();
    }

    //提供一个 schedule() 方法,来注册任务 : 两个参数:
    //第一个参数:执行的 任务
    //第二个参数:执行任务前等待的时间。
    public void schedule(Runnable runnable, long after) {
        //第二个参数这里,需要换算为 : 当前的时刻 + 需要等待的时间。
        MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
        queue.put(task);
        synchronized (this) {
            this.notify(); // 唤醒一下扫描线程
        }
    }


}


//任务的具体描述
class MyTask implements Comparable<MyTask> {
    //要执行的任务内容
    private Runnable runnable;
    // 任务在啥时候执行(使用 毫秒时间戳)
    private long time;

    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }

    //获取当前任务的时间
    public long getTime() {
        return time;
    }

    //执行任务
    public void run() {
        runnable.run();
    }

    @Override
    public int compareTo(MyTask o) {
        // 返回   小于0, 大于0, 0  这三个数字
        // this 比 o 大, 返回  >0;
        return (int)(this.time - o.time);
    }
}





public class ThreadDemo11 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1");
            }
        }, 1000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2");
            }
        }, 2000);
    }
}

        注意:

                1,在创建一个新任务那里,在将任务创建好,放入堆中之后,有一步唤醒操作,这里的唤醒操作是唤醒扫描线程那里的等待。在扫描线程中,会将最优先的任务拿出来看看,如果还没到执行的时间,那就再将任务放回到优先级队列中,并且阻塞需要等待的时间。就是这里,如果在阻塞等待时间内,又来了一个优先级更高的任务(时间更短,比刚刚阻塞等待的时间还短),就需要扫描线程重新去优先级队列中拿出最优先的任务,重新计算阻塞等待的时间,然后阻塞等待。所以说,这里的这个唤醒机制很有必要。 

                2,在扫描线程中,synchronized(this) 这行代码 锁住的是实现MyTimer类 的 对象,同一时间,只能有一个线程可以访问到这个对象。

                拓展:如果类中有多个静态方法,使用synchronized 修饰其中一个静态方法,那也同样是对整个类进行了加锁,同一时间,只能有一个线程可以访问到该类的任何静态方法。但是并没有对实现这个类的对象加锁。


       什么是线程池 

        线程池: 为了使多线程开发更高效,使多线程的使用更轻便,而产生。事先把需要使用的线程创建好,放到“池”中,后面需要使用的时候,直接从池里获取,用完后也还给 “池”,  这两个动作要比 创建/销毁 更高效。

        创建线程/销毁线程 是交给 操作系统内核 完成的。而从池子里获取/还给池, 是咱们自己用户代码就能实现的,不必交给内核操作。

        什么是操作系统内核?

               答:操作系统内核是操作系统最基本的部分,是一个提供硬件抽象层、磁盘及文件系统控制、多任务等功能的系统软件。它是为众多应用程序提供对计算机硬件的安全访问的一部分软件,这种访问是有限的,并且内核决定一个程序在什么时候对某部分硬件操作多长时间。直接对硬件操作是非常复杂的,所以内核通常提供一种硬件抽象的方法来完成这些操作。

       无论是商业的还是个人开发的操作系统内核,都被视为计算机系统的基石和黑盒。这意味着用户通常不需要知道内核内部是如何实现的,只需要使用该内核提供的服务即可。

       我们不清楚内核的具体行为,也就意味着不可控,当我们将任务交给操作系统内核,何时等到系统的回应也是不可控的,因为系统内核不仅仅这一个任务。所以,相比于内核来说,用户态,执行程序的行为是可控的

        线程池的使用:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 使用标准库中的线程池
public class ThreadDemo12 {
    public static void main(String[] args) {
        // 创建了一个固定线程数目的线程池
        // 这里的创建 使用了工厂模式
        ExecutorService pool = Executors.newFixedThreadPool(10);
        
        for(int i = 0; i <1000; i++) {
            int n = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {       // 这个 run() 方法 不是由主线程调用的,
                    // 而是由线程池中的线程调用。
                    System.out.println("hello " + n);
                }
            });
        }
    }
}
什么是工厂模式?

        答:用一句话表示:使用普通方法,来代替构造方法创建对象。那为什么需要代替构造方法了?因为在某些情况下,我们可能要构造多个不同情况的对象,但是使用构造方法的重载又有一些局限性(重载方法 名称相同,参数个数和类型不同),这种情况对我们实现一些代码时有些限制,于是就有了工厂模式。

        普通方法,方法名字没有限制的,因此有多种方法构造,就可以直接使用不同的方法名即可,此时,方法的参数是否要区分就已经不重要了。

自己实现一个线程池:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool {

    // 此处不涉及到时间, 此处只有任务,就直接使用 Runnable 即可
    // 阻塞队列中元素的类型为 Runnable 接口类型
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();


    // n 表示线程的数量
    public  MyThreadPool(int n) {
        // 在这里创建线程
        // for循环来创建线程
        for(int i = 0; i < n; i++) {
           
            Thread t = new Thread(() -> {
                
               // while 循环来让线程不断地从队列中取任务。
                while(true) {
                    try {
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }

    // 注册任务给线程池
    public void submit (Runnable runnable) {
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}


public class ThreadDemo16 {
    public static void main(String[] args) {
        MyThreadPool pool = new MyThreadPool(10);
        for(int i = 0; i < 1000; i++) {
            int n = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello " + n);
                }
            });
        }
    }
}

       ThreadPoolExecutor 类

        

 ThreadPoolExecutor : 最原生的线程池

        ThreadPoolExecutor是Java中线程池的核心实现类,它主要用来执行被提交的任务。通过ThreadPoolExecutor的execute()方法,用户可以提交Runnable任务进行执行;而通过submit()方法,用户不仅可以提交Runnable任务和Callable任务,还能获取异步的执行结果。

       使用ThreadPoolExecutor的主要优点在于,当系统中频繁地创建线程时,如果线程过多,会带来调度开销,进而影响缓存局部性和整体性能。而通过使用线程池,可以避免在处理短时间任务时频繁地创建与销毁线程所带来的代价。线程池维护着多个线程,等待着监督管理者分配可并发执行的任务,这既保证了内核的充分利用,又防止了过分调度。

什么是Runnable 任务?

        在Java中,Runnable接口表示一个可以被线程执行的任务,它本身是一个抽象任务,只定义了要执行的操作,并没有具体的实现。这个接口里包含一个无返回值的方法run()。Runnable没有启动线程的能力,因此必须使用Thread类中的start方法才能够启动一个线程。Runnable的run()方法定义没有抛出任何异常,所以任何的Checked Exception都需要在run()实现方法中自行处理。

什么是 Callable 任务?

        与Runnable相似的Callable接口也能被线程执行,但Callable接口的task能返回一个结果,也可以抛出Exception。两者都可以被ExecutorService执行,其中Callable的call()方法只能通过ExecutorService的submit(Callable task)方法来执行,并且会返回一个Future,是表示任务等待完成的对象。

获取异步的执行结果 是什么意思?

        异步执行结果是指在程序执行过程中,某个耗时较长的操作(通常是IO操作或者计算密集型任务)在执行时并不会阻止其它操作的进行,当这个耗时操作完成时,该操作的结果将会被后续的操作或者函数使用。异步编程是一种提高程序性能的方式,它允许同一时间发生(处理)多个事件。

        例如,在Java中,当我们调用一个耗时较长的功能(方法)时,如网络请求或大规模计算,这个方法并不会阻塞程序的执行流程,程序会继续往下执行。当这个功能执行完毕时,比如数据接收完毕或者计算完成,程序能够获得执行完毕的消息或能够访问到执行的结果(如果有返回值或需要返回值时)。

        另外,在更现代的编程模式中,如回调函数、Promises、Futures和CompletableFuture等,可以以非阻塞的方式获取任务执行结果,这种方式不仅提高了程序的响应速度和执行效率,而且使得代码逻辑更加清晰易懂。

ThreadPoolExecutor类的构造方法有7个参数,

分别是:

        1. corePoolSize:线程池中会维护一个最小的线程数量,这些线程处理空闲状态,他们也不会 被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。

        2. maximumPoolSize:线程池允许的最大线程数量。

        3. keepAliveTime:当线程池中的线程数量超过corePoolSize时,多余的空闲线程的存活时间。

        4. unit:keepAliveTime的时间单位。

        5. workQueue:任务队列,用于存放待执行的任务。

        6. threadFactory:创建新线程的工具类。

        7. handler:当线程池中的线程数量超过maximumPoolSize且任务队列已满时,如何处理新提交的任务。

注解:

        1,corePoolSize 核心线程数 和 maximumPoolSize 最大线程数 的区别:

       核心线程数是指线程池中一直保持的线程数,哪怕它们处于空闲状态。这意味着即使线程池中没有任务,这些核心线程也会一直保持存在,以便于快速响应新的任务请求

       最大线程数则是指线程池中允许的最大线程数,这包括了空闲线程和正在工作的线程。如果线程池的任务队列已满且所有的核心线程都在工作,那么此时如果有新任务提交,线程池会根据设置的策略决定是否创建新的线程

       举两个例子:

                1),在一家公司中, 核心线程数就是正式员工, 最大线程数就是 正式员工 + 实习生;当所有的正式员工都在忙碌,并且还有新的任务下来,这时就会招收一些实习生来缓解压力。如果长时间任务比较少,实习生一直在摸鱼(线程空闲),那么就会销毁这个线程,但是呢核心线程数并不会被销毁(即使线程空闲)

                2),假设你开了一家餐厅,这家餐厅的服务员就是线程,而顾客就是任务。核心线程数就好比是你固定的服务员人数,无论餐厅是否忙碌,这些服务员都会在岗位上待命,随时准备为顾客服务。           最大线程数则好比是餐厅能够容纳的最大服务员数量,包括正在工作的和待命的。如果所有的服务员都在工作,并且还有新的顾客进来,那么就需要根据餐厅的规定来决定是否需要再雇佣新的服务员。                   例如,你的餐厅规定,当所有服务员都在工作时,如果有新的顾客进来,那么就不能再接待更多的顾客了,除非有服务员完成他们的工作并腾出位置。这就是最大线程数的作用。

        2,keepAliveTime  简单解释就是 实习生可以摸鱼的最大时间,超过这个线程就销毁。unit: keepAliveTime 是摸鱼时间的时间单位

        3,handler  其实就是一个拒绝策略  ThreadPoolExecutor类的构造方法中,处理提交任务超过线程池最大容量的拒绝策略有四种:

                1) AbortPolicy(默认):直接抛出RejectedExecutionException异常,阻止系统正常运行。

                2) DiscardOldestPolicy:丢弃等待队列中最旧的任务,然后重新尝试执行任务(重复此过程直到能够执行任务为止)。

                3) DiscardPolicy:直接丢弃新来的任务,不抛出异常。

                4) CallerRunsPolicy:让调用者自己运行任务。

Java编程--定时器/线程池/工厂模式/ ThreadPoolExecutor-LMLPHP


        我是专注学习的章鱼哥~

11-14 02:40