当前位置:   article > 正文

Java线程池

Java线程池

   

目录

一、什么是线程池

二、线程池有哪些好处?

     ①降低资源的消耗

      ②提高响应速度

      ③提高线程的可管理能力

 三、线程池如何使用

       ①创建线程池​编辑

       工厂模式:

      工厂模式代码实现:

②往线程池当中添加任务

 四、Java当中有哪些线程池  ​编辑

    ①Executors.newFixedThreadPool

     ②Executors.newCacheThreadPool

     ③Executors.newScheduledThreadPool

     ④Executors.newSingleThreadExecutor

五、线程池的工作过程

      ①判断线程池当中运行线程的数量是否到达核心线程数量corePoolSize

       ②判断工作队列(存放任务的阻塞队列)是否已经满了

       ③再次创建新的线程

  经典面试问题:已知CPU的核心数为N,那么需要的线程池的线程数量是多少呢?

        (1)对于CPU密集型的任务:

       (2)对于IO密集型的任务

六、线程池ThreadPoolExecutor有哪些参数

    ①核心线程数量(corePoolSize)

    ②任务队列(runnableTaskQueue)

为什么线程池当中需要使用阻塞队列 

    ③最大线程数量(maximumPoolSize)

    ④keepAliveTime

     ⑤unit

     ⑥ThreadFactory

     ⑦RejectedExceptionHandler(饱和策略,也称为拒绝策略)

       (1)直接抛出异常

       (2)只用调用者所在线程来执行任务

       (3)丢弃队列当中的最近一个任务

       (4)不处理最新提交的任务 

七、设计一个简单的线程池       

八、为什么阿里巴巴开发规范禁用Executors

从构造方法分析

      从阻塞队列的长度分析


一、什么是线程池

       线程池,就是一个存放线程的池子。

       它的特点是,在使用线程之前,就一次性把多个线程创建好,放到"池”当中。

       后面需要执行任务的时候,直接从"线程池"当中通过线程执行。

       当线程执行完毕之后,也不是直接销毁线程,而是把线程重新放回到"池子“当中,等需要再次执行任务的时候,就再次从池子当中获取线程,执行任务。

      


二、线程池有哪些好处?

     ①降低资源的消耗

       通过重复利用已经创建的线程降低线程创建,销毁造成的开销。

       我们知道,传统的操作当中,通过调用thread.start()来创建线程,这一个步骤,实际上是调用了操作系统提供的api,来创建PCB,这一个过程涉及操作系统内部的系统调用。

       当线程执行完自己的任务之后,重新销毁线程。这个销毁的过程,也是交给操作系统完成的。

      而如果使用了线程池,可以有效减少不断创建、销毁线程带来的损耗。

      因为线程池创建之后,每次执行任务,都是从线程池当中获取线程,来执行任务,不用每次都通过调用api来获取。


      ②提高响应速度

         当任务需要执行的时候,任务可以不需要等待到线程创建,就可以立即执行任务。


      ③提高线程的可管理能力

         线程属于稀缺资源,如果无限制地创建线程,不仅仅会消耗系统资源,还会降低系统的稳定性。       

         如何理解所谓的"稳定性"呢?

         相比于把线程的获取交给操作系统内核来完成,通过线程池直接获取线程,可以提高程序的"可控性"。

         因为,如果直接创建线程,程序执行的时候,无法预知操作系统的内核当中究竟背负了多少的任务。操作系统内核也许背负了成千上万的任务......那轮到我需要执行的"创建线程"这个任务的时候,究竟响应速度怎样,能否响应都是问题......

         这样,也就无法预知通过操作系统获取线程的真实情况,也就提高了管理线程的难度。

        如果使用线程池来管理线程,那么如何从线程池当中获取线程,使用线程,这个过程都是可以控制,可以预料的。


 三、线程池如何使用

       ①创建线程池

        此处,10指定的是线程池当中的核心最大线程数量,后面会提及。

        线程池创建线程的方式,是通过"工厂模式"来实现的,是通过一个工厂来实现的。

        这里创建线程对象,并没有直接new N个线程对象,而是通过一个特定的工厂(ThreadFactory)来生产线程,后面会介绍到。


       工厂模式:

       可以简单地理解为:使用普通方法,代替构造方法来创建对象。

       如果想要获取对象,只能通过这个"普通方法"来获取对象。

        并且,可以通过这个"工厂"来获取多个对象。


      工厂模式代码实现:

        场景,如果我想构造一个点,根据数学的常识,可以使用极坐标的方式构造,也可以使用半径来构造,这两种构造方式,如下图所示:   


       可以看到,此时发生了编译报错。原因:构造方法重载的方式不正确,此时两个构造方法的参数列表完全相同。

       那如果我仍然想使得传入的参数类型一样,但是产生出不同含义的对象呢?那就需要使用到”工厂模式"。其中一个工厂,负责生产x,y轴构造的对象。另外一个负责生产r,a构造的对象。

     


②往线程池当中添加任务

         在①当中,已经创建好10个线程的任务了,那么,线程的任务,如果执行呢?

         此时,就需要使用submit()方法来把任务放到线程池当中,如下代码:

        

      上图代码的含义是,往一个线程池当中初始化10个线程,然后再往这个线程池当中提交1000个任务,由10个线程来"平均"分配一下这1000个任务。

       但是,不一定是每个线程都可以平均执行100个任务。尽管线程调度有记帐系统来尽可能让每个参与调度的线程执行的时间比较平均,但是也不可以百分百确保每个线程执行获取的任务数量都一模一样。


 四、Java当中有哪些线程池  


目前所见的线程池,本质上都是包装Java当中的一个原生类:ThreadPoolExecutor来实现的

下面将简单认识几个常见的:    

    ①Executors.newFixedThreadPool

       普普通通的线程池,没有什么其他额外的功能。仅仅是用于初始化指定数量的线程,然后添加任务,供自己初始化的线程来执行。


     ②Executors.newCacheThreadPool

       这个线程池,里面的线程数量是动态变换的。

       如果线程池当中的任务比较多,那么将会多初始化一些线程;如果任务比较少,那么将会少初始化一些线程


     ③Executors.newScheduledThreadPool

      这个线程池有点类似于"计时器",让任务拥有延时执行的功能。

      但是这些任务的具体执行是由线程池当中的线程执行。


     ④Executors.newSingleThreadExecutor

        这个线程池当中,只允许拥有一个线程。


五、线程池的工作过程

      ①判断线程池当中运行线程的数量是否到达核心线程数量corePoolSize

  如果线程池当中运行的线程数量没有到达核心线程数量,就会创建一个新的工作线程来执行任务。

  如果线程池当中运行线程数量>=corePoolSize,那么会进入②


       ②判断工作队列(存放任务的阻塞队列)是否已经满了

如果工作队列没有满,那么就把新提交的任务存放到这个工作队列当中,等待线程池当中的线程来执行这个任务。

如果工作队列也满了,那么会进入③


       ③再次创建新的线程

       再次创建新的工作线程,让新的工作线程来执行新增的任务。

      

       如果创建的新的线程让当前线程池的运行线程数量超出maximumPoolSize,将会触发拒绝策略。


总的图解:

  经典面试问题:已知CPU的核心数为N,那么需要的线程池的线程数量是多少呢?


        (1)对于CPU密集型的任务:

       每个线程都需要进行一系列的算术运算,可以理解为此时线程需要狂转CPU。

       那么,此时线程池的线程数量,最大不应该超过CPU的核心数量(N)。

        原因:

      CPU密集型的任务,意味着线程需要比较多的时间在CPU内核上面运行。

       那么,此时应该尽可能让线程都运行在CPU上面,不应该有比较多的线程调度次数。也就是,让每一个线程都尽可能都一直占用一个CPU内核,减少调度的次数,这样程序执行的效率会比较高。


       (2)对于IO密集型的任务

       每一个线程的工作都是等待IO,例如读写硬盘,读写网卡这些比较耗时的任务。

       当执行这些任务的时候,线程大部分是处于阻塞状态,不参与CPU的调度。

       这个时候,理论上线程数量远超过CPU的核心数量也是可以的。但是具体的线程数量,需要结合开发的实践来进行决定。


六、线程池ThreadPoolExecutor有哪些参数


    ①核心线程数量(corePoolSize)

       如果把一个线程池比作一家公司,那么corePoolSize可以理解为这个公司当中的正式员工的总数量。     

       即使这些线程没有在执行任务,也不会被"销毁".

       如果新添加任务的时候,当前运行的线程没有达到核心线程数量(corePoolSize),就会创建新的线程来执行添加的任务。


        并且,当线程当中运行的线程没有达到corePoolSize的时候,即使线程一直空闲着,也不会被销毁。

       我们执行下面这段代码,创建线程池的时候:

ExecutorService pool= Executors.newFixedThreadPool(10);

  指定的参数,就是这个corePoolSize 


    ②任务队列(runnableTaskQueue)

     用于存放执行等待任务的阻塞队列,这个任务队列生效的情况在于:

      运行的线程数量>=核心线程数量,就会把任务添加到这个队列当中。 

    对于阻塞队列的列举已经在这一篇文章当中说明:        Java阻塞队列_革凡成圣211的博客-CSDN博客https://blog.csdn.net/weixin_56738054/article/details/128533816?spm=1001.2014.3001.5501


为什么线程池当中需要使用阻塞队列 

  • 线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲。
  • 另外一方面,如果新任务的到达速率超过了线程池的处理速率,那么新到来的请求将累加起来,这样的话将耗尽资源。

    ③最大线程数量(maximumPoolSize)

        一个公司当中,有正式的员工,也会有实习生/临时工。公司的总人数就是:正式员工的数量+实习生/临时工的总数量。同样地,可以理解为:一个线程池当中总的线程数量(maximunPoolSize)为:核心线程数量+空闲线程的数量。

        最大线程数量可以理解为,此时线程池已经无法再次容纳更多的线程了.

        如果在工作队列已经满了的情况下面,创建新的线程将使得当前运行的线程超出maximunPoolSize,将触发拒绝策略。

       反之,如果创建新的线程没有使得当前运行的线程超过maximunPoolSize,那么线程池就会继续创建新的线程来执行任务。

       


    ④keepAliveTime

       这个属性生效的时间在于,当运行的线程数量超过corePoolSize,并且当有线程处于空闲状态的时间超过KeepAliveTime之后,将会被销毁。


     ⑤unit

        空闲线程存活时间单位,也就是keepAliveTime的单位


     ⑥ThreadFactory

        线程工厂,线程池创建一个新的线程时候使用的"工厂",这个涉及到"工厂模式".


     ⑦RejectedExceptionHandler(饱和策略,也称为拒绝策略)

       当队列和线程池都已经满了,说明线程池处于饱和状态,那么必须采取一系列策略来处理新提交的任务。下面,一共有4种拒绝策略:

       (1)直接抛出异常


       (2)只用调用者所在线程来执行任务

       可以理解为,哪个线程在当前线程池满了之后再次提交任务,那么它提交的任务,不再交给线程池当中的线程执行,而是自己执行自己提交过的任务。


       (3)丢弃队列当中的最近一个任务

        线程池当中的任务都是被排好序的,这种拒绝策略就是丢掉正在执行的任务,执行最新提交的任务。


       (4)不处理最新提交的任务 

        直接丢弃线程池满了之后,新增的任务,不予执行。


七、设计一个简单的线程池       

      ①需要一个阻塞队列,保存添加的任务;

      ②构造方法指定线程池当中线程的数量;

      ③submit()方法往线程池当中提交任务(runnable):


      代码实现:

  1. class MyThreadPool {
  2. /**
  3. * 阻塞队列,用来保存任务
  4. */
  5. private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
  6. /**
  7. * 创建n个线程
  8. * 线程数量@param n
  9. */
  10. public MyThreadPool(int n) {
  11. //在这里创建
  12. for (int i = 0; i < n; i++) {
  13. Thread t = new Thread(new Runnable() {
  14. @Override
  15. public void run() {
  16. while (true) {
  17. try {
  18. //取出线程
  19. Runnable runnable = queue.take();
  20. //执行任务
  21. runnable.run();
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. }
  25. }
  26. }
  27. });
  28. t.start();
  29. }
  30. }
  31. public void submit(Runnable runnable) {
  32. try {
  33. queue.put(runnable);
  34. } catch (InterruptedException e) {
  35. e.printStackTrace();
  36. }
  37. }
  38. }

八、为什么阿里巴巴开发规范禁用Executors

最主要的原因:这些线程池都存在内存溢出(oom)的可能性:也就是"内存用完了"。

具体分析如下:

从构造方法分析

       关于这些参数的含义,已经在上面的文章当中提到了。

       下面,重点关注这一个参数:阻塞队列。这个参数的含义就是:当线程池的核心线程都处于非空闲的状态的时候,如果添加任务,就会把任务(Runnable)加入到这一个阻塞队列当中。

      从阻塞队列的长度分析

        然后,再看一下默认调用无参数的构造方法的时候,阻塞队列的最大长度:

这个参数的含义就是当前阻塞队列的最大长度

而这个最大长度,就是整形的最大值。也就意味着当前的阻塞队列的长度是Integer.MAX_VALUE

那么也就意味着,这样容易出现oom(Out of Memory Killer)也就是内存溢出。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/498828
推荐阅读
相关标签
  

闽ICP备14008679号