[并发编程]一文搞懂线程池

在面试过程中,并发编程是绕不过去的话题,其中线程池是其中的重中之重,所以今天就来说下关于线程池的那些事。本人以前曾经使用多线程实现过表格导入,但是经过一段时间的学习之后,发现当时的实现方式简直太蠢了,也希望以此记录下

创建多线程的方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口
  4. 使用线程池

线程池的创建方式

(1)Executors.newFixedThreadPool(5):创建固定数量的线程池(一池固定线程数)
原理:创建一个线程池,核心线程数为5,最大线程数也为5,当任务来了,先交给常驻核心线程来执行,超过核心线程数的任务将会放到阻塞队列中(这里只简单说下,后面会详细解释下线程池工作过程)

new ThreadPoolExecutor(
                  nThreads,//核心线程数
                  nThreads,//最大线程数(与核心线程数相同)
                  0L, //存活时间(非核心线程的存活时间)
                  TimeUnit.MILLISECONDS,//存活时间的单位
                  new LinkedBlockingQueue<Runnable>()//阻塞队列(存放任务)
                  );

(2)Executors.newSingleThreadExecutor():创建单个线程的线程池
原理:创建一个线程池,核心线程数为1,最大线程数也为1,当任务数超过最大线程数池时,将任务放置到队列中,保证只有一个线程在工作,从而保证按照指定的顺序来执行

new ThreadPoolExecutor(
                  1,//核心线程数
                  1,//最大线程数(与核心线程数相同)
                  0L, //存活时间(非核心线程的存活时间)
                  TimeUnit.MILLISECONDS,//存活时间的单位
                  new LinkedBlockingQueue<Runnable>()//阻塞队列(存放任务)
                  );

3)Executors.newCachedThreadPool():创建可以缓存的线程
原理:创建一个线程池,核心线程数为0,最大线程数Integer.MAX_VALUE即初始化线程池时并没有可用线程,当来了任务之后再进行创建线程去执行;当执行第二个任务时,如果第一个任务已完成(未被回收时),则直接复用第一个任务的线程;如果第一个任务未完成,则创建第二个线程来执行第二个任务;默认可以创建无限多个任务线程


new ThreadPoolExecutor(
                  0,//核心线程数
                  Integer.MAX_VALUE,//最大线程数(与核心线程数相同)
                  60L, //存活时间(非核心线程的存活时间)
                  TimeUnit.MILLISECONDS,//存活时间的单位
                  new LinkedBlockingQueue<Runnable>()//阻塞队列(存放任务)
                  );

(4)newScheduledThreadPool:是可以设置定时执行的线程池,这个不常使用,不做过多解释
(5)手动创建线程池

/**
 * ThreadPoolExecutor参数介绍
 * int corePoolSize,----线程池中的常驻核心线程数,即可以立即执行线程的个数,类似  银行当值窗口
 * int maximumPoolSize,----线程池能够容纳同时执行的最大线程数,即线程池最多可执行线程数,此致必须大于等于1,类似  银行所有窗口
 * long keepAliveTime,----多余的空闲线程的存活时间
 * 当前线程池数量超过corePoolSize时,当空闲线程达到keepAliveTime时,
 * 多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
 * <p>
 * TimeUnit unit,----keepAliveTime的单位
 * BlockingQueue<Runnable> workQueue,任务队列,被提交但尚未被执行的任务   类似   银行的排队座位数量
 * ThreadFactory threadFactory,----表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可
 * RejectedExeutionHandler handle----拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数时,将会拒绝执行部分线程
 */
ExecutorService executorService = new ThreadPoolExecutor(2,
                5,
                2,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
//                new ThreadPoolExecutor.AbortPolicy()//当线程池不能再执行新的任务的时候丢弃任务并抛出RejectedExecutionException异常。
                new ThreadPoolExecutor.CallerRunsPolicy()//当线程池不能再执行新的任务的时候,将会把新的任务返回给调用者,让调用者去执行
//                new ThreadPoolExecutor.DiscardOldestPolicy()//当线程池不能执行新的任务的时候,将会抛弃等待时间最长的任务,然后重新尝试提交最新的任务
//                new ThreadPoolExecutor.DiscardPolicy()//当线程之不能执行新的任务的时候,直接抛弃新任务,不执行也不报错
        );

为啥不建议使用Executors去创建线程池的原因

(1) newFixedThreadPool和newSingleThreadExecutor主要问题是其阻塞队列的大小默认值为Integer.MAX_VALUE,即默认允许创建的线程数为Integer.MAX_VALUE,看似有界队列,实为无界队列,可能会创建大量的线程,从而导致OOM

(2) newCachedThreadPool和newScheduledThreadPool主要问题是其最大线程数默认为Integer.MAX_VALUE,可能会堆积大量的请求,这些请求会耗费大量的内存资源,从而导致OOM

线程池的七大参数在这里插入图片描述


/* *ThreadPoolExecutor参数介绍
 * int corePoolSize:线程池中的常驻核心线程数,即可以立即执行线程的个数,类似  银行当值窗口
 * int maximumPoolSize:线程池能够容纳同时执行的最大线程数,即线程池最多可执行线程数,此致必须大于等于1,类似  银行所有窗口
 * long keepAliveTime:,----多余的空闲线程的存活时间
 * 当前线程池数量超过corePoolSize时,当空闲线程达到keepAliveTime时,
 * 多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
 * <p>
 * TimeUnit unit:-keepAliveTime的单位
 * BlockingQueue<Runnable> workQueue:任务队列,被提交但尚未被执行的任务   类似   银行的排队座位数量
 * ThreadFactory threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可
 * RejectedExeutionHandler handle:-拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数时,将会拒绝执行部分线程
 */

线程池的参数理解

上面的线程参数要理解的话,我想有个例子是很合适的,那就是银行窗口
常驻核心线程数----当值窗口
非核心线程数-------暂停营业窗口
最大线程数---------所有窗口=当值窗口+暂停营业窗口
空闲线程存活时间-------暂停营业窗口临时营业时间
单位--------暂停营业窗口临时营业时间单位
阻塞队列------------等候区
拒绝策略-------------大堂经理,来给新来的群众解释拒绝办理业务的原因和方案
正常情况下,银行当值窗口开了4个,加上等候区有7个位置,可满足对11个群众服务
当值窗口未满时,新来群众直接去窗口办理业务
当值窗口坐满时,新来群众要到等候区等待
某天,业务异常火爆,当值窗口已满,等候区已满,此时还有群众来办业务
大堂经理就要去打电话给休息的同事,同时开启暂停营业的窗口(最大线程数开满),并说如果在30分钟(存活时间)内窗口没业务,你可以继续休息,窗口关闭
然而7个窗口+7个等候区都不能满足时,大堂经理就要将群众拒之门外了(拒绝策略)

在这里插入图片描述

在这里插入图片描述

线程池的拒绝策略

new ThreadPoolExecutor.AbortPolicy()//当线程池不能再执行新的任务的时候丢弃任务并抛出RejectedExecutionException异常。
new ThreadPoolExecutor.CallerRunsPolicy()//当线程池不能再执行新的任务的时候,将会把新的任务返回给调用者,让调用者去执行
new ThreadPoolExecutor.DiscardOldestPolicy()//当线程池不能执行新的任务的时候,将会抛弃等待时间最长的任务,然后重新尝试提交最新的任务
new ThreadPoolExecutor.DiscardPolicy()//当线程之不能执行新的任务的时候,直接抛弃新任务,不执行也不报错
实现RejectedExecutionHandler接口,可自定义处理器

线程池的底层工作原理

1)在创建了线程池后,等待提交过来的任务请求
(2)当调用execute()方法添加一个请求任务时,线程池会做如下判断
    -2.1  如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
    -2.2  如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
    -2.3  如果此时队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
    -2.4  如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
(3)当一个线程完成任务时,它会从队列中取下一个任务来执行
(4)当一个线程无事可做超过一定的随时间(keepAliveTime)时,线程池会判断
    如果当前运行的线程数大于corePoolSize,那么这个线程就会被停掉

如何确定最大线程数

参考文章:如何合理的估算出线程池中的核心线程池个数的大小

分析一般从几个角度考虑:
1.任务的性质:CPU密集型的任务、IO密集型任务、混合型任务。
2.任务的优先级:高、中、低
3.任务执行时间:长、中、短
4.任务的依赖性:是否依赖其它系统资源,如数据库的连接等。
根据不同的任务可以交给不同规模的线程池执行。
如果是cpu密集型的,尽量减少线程数,如果是IO密集型任务尽量加大线程数,因为io不占用cpu的资源。建议配置2倍CPU个数+1。
如果是混合型的,尽量根据实际情况进行拆分,根据运行时间来决定。
如下为一般计算公式:
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程
高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
(2)并发不高、任务执行时间长的业务要区分开看:
  a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以适当加大线程池中的线程数目,让CPU处理更多的业务
  b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

这里也推下我的公众号:总要有一个梦想或大或小
在这里插入图片描述

最新文章会第一时间发到公众号,感谢关注

欢迎评论指正

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页