【并发编程-基础】(八)线程池

文章目录

一、new Thread的弊端

  • 每次new Thread新建对象,性能差。
  • 线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或者OOM(不单单因为单独new Thread,可能会有设计上的缺陷,不断地触发new Thread)。
  • 缺少更多的功能,如更多执行、定期执行、线程中断。

二、线程池的好处

  • 重用存在的线程,不用每次都创建线程,减少对象创建、消亡的开销,性能佳。
  • 可以有效的控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞
  • 提供定时执行、定期执行、单线程、并发数控制等功能

三、线程池核心类-ThreadPoolExecutor

3.1、线程池的三个参数

       3.1.1、corePoolSize(核心线程数量)
       3.1.2、maximumPoolSize(线程最大线程数)
       3.1.3、workQueue(阻塞队列)
  • 用于存储等待执行的任务,很重要,会对线程池运行过程产生重大影响
       3.1.4、keepAliveTime(线程没有任务执行时最多保持多久时间终止)
       3.1.5、unit(keepAliveTime的时间单位)
       3.1.6、threadFactory(线程工厂)
  • 来创建线程(默认会有一个默认的工厂来创建线程,使用默认的工厂创建线程时,会使新创建的线程具有相同的优先级,并且是非守护的线程,同时也设置了线程的名称)
       3.1.7、rejectHandler(拒绝处理任务时的策略)

四种拒绝处理的策略

1、直接抛出异常(默认)AbrotPolicy;

2、用调用者的线程来执行任务CallerRunsPolicy;

3、丢弃队列中最靠前的任务并执行当前任务DiscardOldstPolicy;

4、直接丢弃这个任务DisccardPolicy

3.2、任务提交时线程池的工作

       3.2.1、任务提交时线程池的参数对任务处理的关系
  • 如果运行线程数小于corePoolSize,将直接创建新线程来处理任务,即使线程池中的其他线程是空闲的。
  • 如果线程池的线程数量大于等于corePoolSize的值且小于maximunPoolSize的值时,只有当workQueue满的时候才创建新的线程去处理任务。
  • 如果corePoolSize和maximumPoolSize相同的话,那么创建线程池的大小是固定的,这个时候如果有新用户提交,且workQueue没满的时候就把请求放入到workQueue里面,等待空闲的线程,然后再从workQueue里面取出任务。
  • 如果运行的线程数大于maximumpoolSize,如果workQueue也满了,那么就会通过后面介绍的一个拒绝策略的参数指定策略去处理这个任务。
  • 所以在任务提交的时候,顺序主要有三个,第一个是corePoolSize,小于它就直接创建线程并调用了。下一个就是workQueue,最后就是是maximumuPoolSize。
       3.2.2、提交新任务时线程池的三种处理方式
  • workQueue是保存等待执行的任务的一个阻塞队列,当我们提交一个新的任务到线程池之后,线程池会根据当前线程池中正在运行的线程的数量来决定该任务处理的方式,处理方式有三种
(1)直接切换(SynchronusQueue)
(2)使用无界队列(LinkedBlockingQueue)
  • 能够创建的最大线程数为corePoolSize,这时maximumPoolSize就不会起作用了。
  • 当线程池中所有的核心线程都是运行状态的时候,新的任务提交就会放入等待队列中。
(3)使用有界队列(ArrayBlockingQueue)
  • 最大maximumPoolSize,能够降低资源消耗,但是这种方式使得线程池对线程调度变的更困难。
  • 因为线程池与队列容量都是有限的。所以想让线程池的吞吐率和处理任务达到一个合理的范围,又想使我们的线程调度相对简单,并且还尽可能降低资源的消耗,我们就需要合理的限制这两个数量。

3.3、线程池的分配技巧

  • 如果想降低资源的消耗包括降低cpu使用率、操作系统资源的消耗、上下文切换的开销等等,可以设置一个较大的队列容量和较小的线程池容量,这样会降低线程池的吞吐量。
  • 如果我们提交的任务经常发生阻塞,我们可以调整maximumPoolSize。如果我们的队列容量较小,我们需要把线程池大小设置的大一些,这样cpu的使用率相对来说会高一些。
  • 但是如果线程池的容量设置的过大,提高任务的数量过多的时候,并发量会增加,那么线程之间的调度就是一个需要考虑的问题。这样反而可能会降低处理任务的吞吐量。

四、线程实例的状态

  • 这些状态不用处理,是线程池内部做的这些处理。

4.1、RUNNING

       能接收新提交的任务,并且能处理阻塞队列中的任务。

4.2、SHUTDOWN

       属于关闭状态,当一个线程池实例处于SHUTDOWN状态的时候,不能再接受新提的任务,只能继续处理阻塞队列中已经保存的任务。在线程池处于RUNNING状态时,调用shutdown()方法时,会使线程池进入到这个状态。

4.3、STOP

       不接受新的任务,也不处理队列中的任务,会中断正在处理任务的线程,在线程池处于RUNNING或SHUTDOWN状态时如果调用了shutdownNow()方法,线程池就会进入到该状态。

4.4、TIDYING

       如果所有的任务都已经终止了,这个时候有效线程数为0,线程池会进入到该状态,之后调用terminated()方法,会进入到TERMINATED的状态,默认的terminated()方法什么也不做,只是进入了最后一个状态。

4.5、TERMINATED

       最终状态。

五、ThreadPoolExecutor提供的方法

5.1、基本的方法:

  • execute():提交任务,交给线程池执行
  • submit():提交任务。能够返回执行结果 execute+Future
  • shutdown():关闭线程池,等待任务都执行完。关闭线程池节省系统资源
  • shutdownNow():关闭线程池,不等待任务执行完,也会暂停正在执行的线程。适用于不得不暂停所有的任务,这种情况很少。

5.2、监控的方法:

  • getTaskCount():线程池已执行和未执行的任务总数
  • getCompletedTaskCount():已完成的任务数量
  • getPoolSize():线程当前的线程数量
  • getActiveCount():当前线程池中正在执行任务的线程数量

5.3、方法图

       

六、Executor框架接口与常用方法

6.1、Executor线程池类图

在这里插入图片描述

6.2、Exector的四大接口解析

       6.2.1、Executor
  • Executor是运行新任务的简单接口。
       6.2.2、ExecutorService
  • ExecutorService扩展了Executor接口,添加了一些用来管理执行器生命周期和任务生命周期的方法。
       6.2.3、ScheduledExecutorService
  • ScheduledExecutorService扩展了ExecutorService方法,它支持Future和定期执行任务
       6.2.4、ThreadPoolExecutor
  • ThreadPoolExecutor是图里面功能最强的,因为可以根据我们自己需要传入我们需要的参数以及指定任何策略,因此才把他放在最前面讲

6.3、Executor创建的四种线程池

       6.3.1、newCachedThreadPool(创建可缓存的线程池)
  • Executors.newCachedThreadPool:创建的是一个可缓存的线程池,如果线程池长度,超过了处理的需要,可以灵活回收空闲线程,如果没有可以回收的,就新建线程。
       6.3.2、newFixedThreadPool(创建定长的线程池)
  • Executors.newFixedThreadPool:创建的是一个定长的线程池,可以控制线程的最大并发数,超出的线程会在队列中等待。
       6.3.3、newScheduledThreadPool(定长的线程池,支持定时以及周期性的任务)
  • Executors.newScheduledThreadPool:创建的也是一个定长的线程池,支持定时以及周期性的任务。
       6.3.4、newSingleThreadExecutor(创建单线程化的线程池)
  • Executors.newSingleThreadExecutor:创建的是一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序去执行,可以按照先入先出,优先级等等去执行。

七、合理配置线程池

7.1、配置线程池原则

  • CPU密集型任务,就需要尽量压榨CPU,参考值可以设为NCPU+1

  • IO密集型任务,参考值可以设置为2*NCPU

  • 可以先将线程池大小设置为参考值,再观察任务运行情况,系统负载,资源利用率等等来进行适当调整。

7.2、配置线程池需注意

  • 使用线程池主要是为了同用存在的线程,减少对象创建消亡,能有效的控制并发线程数,可以避免过多的资源竞争,避免阻塞,也可以定时执行,单线程等控制性能的执行,性能比较好。当然不代表要随时随地的拿出线程池来用,一定要根据自己的实际场景来分析使用,以及参数配置。
  • 有时候用线程池没有直接进行来的快:当线程池里面的任务数很小时,小到任务进行的时间与任务调度的时间很接近的时候,这时候用线程池,反而会格外慢,因为花在任务调度和任务管理的时间就会更多。
发布了20 篇原创文章 · 获赞 1 · 访问量 559

猜你喜欢

转载自blog.csdn.net/weixin_42295814/article/details/103792458