• (JavaEE)(多线程案例)线程池 (简单介绍了工厂模式)(含经典面试题ThreadPoolExector构造方法)


    目录

    线程池

    池 

    为什么,从池子里取的效率比新创建线程效率高?

    内核态 和  用户态 

    Java标准库中的线程池

    线程池的创建 

     工厂模式

    工厂模式的作用? 

    工厂模式实践 

    不同的几种线程池 

    第一种: 

    第二种 :

    第三种:

    第四种 :

     ThreadPoolExector 的使用方式

    注册任务(简单):submit 

    ⁜⁜ 构造方法⁜⁜【经典面试题】

    ​编辑

     实现一个简单的线程池


     

    线程诞生的意义,是因为进程的创建/销毁,太重了(比较慢),虽然和进程比,线程更快了,但是如果进一步提高线程创建销毁的频率,线程的开销就不能忽视了。

    这时候我们就要找一些其他的办法了。

    有两种典型的办法可以进一步提高这里的效率:

    1: 协程 (轻量级线程,相比于线程,把系统调度的过程给省略了,变成由程序员手工调度)

    (当下,一种比较流行的并发编程的手段,但是在Java圈子里,协程还不够流行,GO和Python用的比较多)

    2:线程池(Java用的)

    接下来我们就来介绍一些线程池 

    线程池

    优化频繁创建销毁线程的场景 

    首先,我们先来了解一下什么是池:

    池 

    假设一个美女,有很多人追,美女就从这些人里面挑了一个她最喜欢的交往,交往一段时间之后,美女她腻了,她想换一个男朋友,那她接下来就要干两件事

    1:想办法和这个男的分手,需要一些技巧,比如挑他毛病,作 之类的

    2:再找一个小哥哥,培养感情,然后交往 

    但是这样的效率就比较低啦,有没有什么办法来提高一些效率呢?

    当然有,只有美女在和前一个交往的时候,和另一个或多个小哥哥,搞暧昧(养🐟),把他们都放到自己的鱼塘里,那和上一个分手,下一个男朋友来的就很快了。 

    我们还是只做了两步,只是把第二步提前了。 

    “鱼塘”里的这些人,我们通常叫他们 —— “备胎” 

    那这个“鱼塘” 就 可以看成 “池” ,来放“备胎”

    同样的,线程池,就是在使用第一个线程的时候,提前把 2,3,4,5...(多个)线程创建好(相对于前面的培养感情),那后续我们想使用新的线程的时候,就不必重新创建了,直接拿里的线程用就行了。(此时创建线程的开销就被降低了)

    为什么,从池子里取的效率比新创建线程效率高?

    这是因为,从池子里取 这个动作,是存粹的 用户态 的操作,而创建新的线程,这个动作,则是需要 用户态 + 内核态 相互配合完成的操作。

    内核态 和  用户态 

    如果一段程序,是在系统内核中执行的,此时就称为“内核态” ,如果不是,则称为“用户态”

    操作系统,是由 内核 + 配套的应用程序 构成的,

    内核:系统最核心的部分

    创建线程操作,就需要调用系统 api,进入到内核中,按照内核态的方式来完成一系列动作。

    内核态的操作要比 纯用户态的操作开销要更大 :至于为什么,我们来举一个例子解释一下:

    银行办业务的例子 

    首先这个来办理业务的人他不能 进入柜台后面,只能在大厅里,

    这个人想来办张银行卡,需要身份证复印件,但是这个人他忘带了,那此时柜台的服务人员就给了他两个选择:

    1:把身份证给她,她去帮他复印

    2:大厅的角落,有一个自助复印机,他可以去那里自己复印 

    那这两个选择中的第二个,自己复印就是纯 用户态操作(这个人可以立即去复印,完事后立即回来办理业务,整个过程非常利落,非常可控

    但是如果交给 柜台的服务人员(第一个选择),这个过程就涉及到 内核态 操作了,那此时,你把东西交给他俩,你也不知道柜员消失之后去做了那些事情,也不是的她啥时候回来,整个过程是不可控的。

    操作系统内核,是要给所有的进程提供服务的,当你要创建线程的时候,内核虽然会帮你做,但是做的过程中难免也要做一些其他的事情。那在你这边的结果,就不是那么可控。

    上述就是内核态 和 用户态的区别 。

    Java标准库中的线程池
     

    线程池的创建 

    我们发现了,线程池这个对象不是我们直接 new 的,而是通过一个专门的方法,返回了一个线程池的对象。 

    这种写法就涉及到了 “工厂模式”(校招常考的设计模式)(和上一篇介绍的 单例模式 并列) 

     工厂模式

    工厂模式的作用? 

    通常我们创建对象 都是使用 new,new 关键字就会触发 类的构造方法,但是构造方法,存在一定的局限性。

    “工厂模式” 就是给 构造方法填坑的。 

     那 “工厂模式” 具体是填的什么 坑 呢,我们举一个例子:

     假设 考虑 一个类,来表示平面上的点

    然后我们给这个类提供构造方法:

    第一个构造方法: 

    期待使用笛卡尔坐标系来构造对象。 

     

    第二个构造方法:

    使用极坐标来构造对象 

    但是编译失败了。 

     

     原因:

    很多时候,我们希望构造一个对象,可以有多种构造方式 。那多种方式,我们就需要使用多个版本的构造方法来分别实现,但是构造方法要求方法的名字必须是类名,不同的构造方法 只能通过 重载 的方式来区分了,而重载又要求 参数类型 或 个数 不同。

    而上面的两个构造方法 很明显没有构成 重载,当然会编译失败。 

    这就是 构造方法的局限性 。

    “工厂模式”就能解决上述问题 :

    使用普通的方法,代替构造方法完成初始化工作,普通的方法就可以使用方法的名字来区分了。也就不受 重载的规则制约了。

    工厂模式实践 

    在实践中,我们一般单独 搞一个类,然后给这个类搞一些静态方法,由这些静态方法负责构造出对象 

    伪代码 

    class PointFactory {
        public static Point makePointByXY(double x, double y) {
            Point point = new Point();
            point.setX(x);
            point.setY(y);
            return p;
        }
        public static Point makePointByRA(double r, double a) {
            //和上边类似
        }
    } 
    
    class Demo {
        public static void main(String[] args) {
            //使用 Point p = PointFactory.makePointByXY(10,20); 
        }
    }
    

    上述介绍之后,我们就知道了为啥 线程池 的 对象我们不直接 new 了

     

    这种方法就是 工厂模式 

    不同的几种线程池 

    第一种: 

    此时构造出的线程池对象,有一个基本特点,线程数目是能够动态适应的。

    cached: 缓存,用过之后不着急释放,先留着以备下次使用。

    也就是说,随着往线程池里添加任务,这个线程池中的线程会根据需要自动被创建出来,创建出来之后也不会着急销毁,会在池子里保留一定的时间,以备随时再使用。

     

    除了上边的线程池,我们还有其他的线程池:

    第二种 :

    这个方法就需要我们指定 创建几个线程,线程个数是固定的 (Fix:固定)

    第三种:

    只有单个线程的线程池: 

    第四种 :

    类似于 定时器, 只是 不是只有一个 扫描线程 负责执行任务了,而是有多个线程执行时间到的任务.

     第一种和第二种常用

    上述这几个工厂方法生成的线程池,本质上都是对 一个类进行的封装 ——  ThreadPoolExector

    ThreadPoolExector 这个类的功能十分丰富,它提供了很多参数,标准库中上述的几个工厂方法,其实就是给这个类填写了不同的参数来构造线程池。 

     ThreadPoolExector 的使用方式

    ThreadPoolExector 的核心方法:

    1.构造方法

    2.注册任务(添加任务)

    注册任务(简单):submit 

    ⁜⁜ 构造方法⁜⁜【经典面试题】

    构造方法中的参数,很多,且重要, 

    我们打开Java文档     Overview (Java Platform SE 8 ) (oracle.com)

    打开这个包  juc —— 这个包里放的试和 “并发编程” 相关的内容(Java中,并发编程最主要的体现形式就是多线程)

    点进这个包然后往下找: 

    然后我们直接翻到构造方法 :

    上面的四个构造方法,都差不多,就是参数个数 不一样,第四个 参数最多,能够涵盖上述的三个版本。 

    所有我们重点看第四个构造方法: 

     

    这一组参数,描述了线程池中,线程的数目: 

    这个线程池里的线程 的数目试可以动态变化的,

    变化的范围就是【corePoolSize, maximumPoolSize】

    那 “核心线程”  和 “最大线程” 如何理解呢?

    如果把一个线程池,理解为一个公司,此时,公司里有两类员工

            1.正式员工

            2.实习生

    那正式员工的数目,就是核心线程数,正式员工 + 实习生的数目就是最大线程数

    正式员工和实习生的区别:

    正式员工,允许摸鱼,不会因为摸鱼被公司开除,有劳动法罩着。

    但是实习生,不允许摸鱼,如果这段时间任务多了,此时,就可以多搞几个实习生去干活,如果过段时间任务少了,并且这样的状态还持续了一定时间,那空闲的实习生就可以裁掉了。

    这样做,既可以满足效率的要求,又可以,避免过多的系统开销 。

    ps: 

     使用线程池,需要设置线程的数目,数目设置多少合适?

     一定不是一个具体的数字!!!因为在接触到实际的项目代码之前,这个数目是无法确定的!!!

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

    1.cpu 密集型:代码里主要的逻辑是在进行 算术运算/逻辑判断。

    2.IO 密集型:代码里主要进行的是IO操作。

    —— 假设一个线程的所有代码都是 cpu 密集型代码,这个时候,线程池的数量就不应该超过N,就算设置的比N大,此时也无法提高效率,因为cpu吃满了。

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

    上述,我们就知道了,代码不同,线程池的线程数目设置就不同,我们无法知道一个代码,具体多少内容是cpu密集,多少内容是IO密集。所以我们无法确定 数目设置多少合适。

    正确做法:使用实验的方式,对程序进行性能测试,测试的过程中尝试修改不同的线程池的线程数目,看那种情况,更符合要求。

    这一组参数,描述了允许实习生摸鱼的时间,(实习生不是 一摸鱼就马上被开除)

     

    这个参数的意思是 阻塞队列 ,用来存放线程池里的任务。

    可以根据需要,灵活设置这里的队列是啥,比如需要优先级, 就可以设置 PriorityBlockingQueue

    如果不需要 优先级,并且任务数目是相对恒定的,可以使用 ArayyBlockingQueue,如果不需要优先级,并且任务数目变动比较大,就可以用 LinkedBlockingQueue

    这个参数就是 工厂模式的体现 ,此处使用 ThreadFactory 作为 工厂类 由这个类负责创建线程

     

    使用工厂类来创建线程,主要是为了在创建线程的过程中,对线程的属性做出一些设置。 

    如果手动创建线程,就得手动设置这些属性,就比较麻烦,使用工厂方法封装一下,就更方便。 

    下面这个参数是最重要的  ,是线程池的拒绝策略

    一个线程池,能容纳的任务数量,有上限,当持续往线程池里添加任务的时候,一旦达到了上限,还继续添加,会出现什么效果?

    拒绝策略就是来解决这个问题的: 不同的拒绝策略有不同的效果。

     上面的这四个就是不同的拒绝策略

    如果队列满了,再添加就直接抛出异常 

    新添加的任务,由添加任务的线程负责执行 

     

    丢弃最老的任务 

    丢弃当前新加的任务 

     实现一个简单的线程池

    这个代码比较简单,就不多说了,代码里都有注释 

    1. import java.awt.*;
    2. import java.util.concurrent.ArrayBlockingQueue;
    3. import java.util.concurrent.BlockingQueue;
    4. import java.util.concurrent.ExecutorService;
    5. import java.util.concurrent.Executors;
    6. /**
    7. * @Author: iiiiiihuang
    8. */
    9. public class ThreadPool {
    10. //任务阻塞队列
    11. private BlockingQueue queue = new ArrayBlockingQueue<>(4);
    12. //通过这个方法,把任务添加到队列中
    13. public void submit(Runnable runnable) throws InterruptedException {
    14. //此处的拒绝策略,相当于第五种策略,阻塞等待(下策)
    15. queue.put(runnable);
    16. }
    17. //构造方法
    18. public ThreadPool(int n) {
    19. //创建出n个线程,负责执行上诉队列中的任务
    20. for (int i = 0; i < n; i++) {
    21. Thread t = new Thread(() -> {
    22. //让这个线程,从队列中消费任务,并执行
    23. try {
    24. //取出
    25. Runnable runnable = queue.take();
    26. //执行
    27. runnable.run();
    28. } catch (InterruptedException e) {
    29. throw new RuntimeException(e);
    30. }
    31. });
    32. t.start();
    33. }
    34. }
    35. }

    关注,点赞,评论,收藏,支持一下╰(*°▽°*)╯╰(*°▽°*)╯

  • 相关阅读:
    Shell变量作用范围
    vue中使用vue-property-decorator
    ajax请求实现学生信息的增改查
    手机远程控制plc有什么优势
    Spring核心扩展点BeanDefinitionRegistryPostProcessor源码分析
    Docker Compose
    @DateTimeFormat 和 @JsonFormat 注解详解
    7.1-WY22 Fibonacci数列
    android MediaStore.ACTION_IMAGE_CAPTURE 调用照相机返回图片太小问题解决方法
    CSDN 重新开放付费资源的上传了,但要求如下
  • 原文地址:https://blog.csdn.net/iiiiiihuang/article/details/133081614