一篇带你彻底搞懂线程池

目录

一、自定义线程池

1、产生背景

2、堵塞队列

3、线程池

4、拒绝策略

二、ThreadPoolExecuor

1、线程池状态

2、构造方法

3、newFixedThreadPool

4、newCachedThreadPool

5、newSingleThreadExecutor

6、提交任务

7、关闭线程池

三、异步模式之工作线程

1、定义

2、饥饿

3、解决

四、多少线程数合适(重点)

1、CPU密集型运算

2、I/O密集型运算

五、任务调度线程池

1、Timer类

2、ScheduledThreadPoolExecutor

3、定时任务

六、Tomcat线程池

1、介绍

2、参数


一、自定义线程池

1、产生背景

线程是系统资源,每创建新的线程都会占用系统内存(分配栈内存),如果高并发场景下,为每个任务都创建个线程(可能out of memory),而且cpu也忙不过来, 会出现频繁上下文切换

我们在学juc提供的线程池之前,先自定义线程池,主要有3部分,thread pool线程池、blocking queue阻塞队列和main消费者

2、堵塞队列

实现阻塞队列首先要有个任务队列queue、锁、生产者条件变量、消费者条件变量、容量。然后阻塞获取的方法,阻塞添加的方法,获取大小的方法。

阻塞获取:上来先循环如果队列是空,说明没有线程可以用就直接调用消费者条件变量休息室去等待,当别人往里面添加了元素就会唤醒一个,然后他就去队列移除一个元素再唤醒一个生产者休息室里的一个,最后返回。

阻塞添加:进来如果队列满了就进入生产者休息室,当消费者移除队列一个线程后会通知他,他就添加一个线程,之后再唤醒消费者。

带超时的阻塞获取:参数带一个超时时间时间单位,先进来调用toNanos方法把时间统一转化为纳秒,然后其他都跟刚刚获取一样,就每次尝试获取是调用awaitNanos返回的是剩余时间,然后循环再进来判断最后超过时间就返回null

带超时的阻塞添加:传入任务对象、超时时间和单位,跟上面是一样的

3、线程池

属性有刚刚我们写的阻塞队列,还有线程集合用hashSet,我们的线程就不用thread了我们包装了个worker类,还有核心线程数,获取任务的超时时间时间单位

执行任务的方法是没有超过核心线程数的时候就交给worker去执行任务,如果超过就往任务队列放任务。

这个worker的run方法就是执行任务的,当任务不为空就执行任务 ,当task执行完毕就从任务队列里面获取执行。当所有任务都执行完成了就直接从集合this中移除

4、拒绝策略

利用策略模式来实现当任务队列满的拒绝策略

tryPut方法直接传入用户输入的函数式变成,然后判断队列是否满,满了就调用用户的方法,如果没有满就加入任务队列。

让用户自己定义拒绝策略

二、ThreadPoolExecuor

1、线程池状态

ThreadPoolExecutor使用int的高3位来表示线程池状态,低29位表示线程数量

状态名高3位接收新任务处理队列任务说明
running111YY
shutdown000NY不会接受新任务了但是会处理堵塞队列中的剩余任务
stop001NN会中断正在执行的任务,并抛弃阻塞队列的任务
tidying010--任务全部执行完毕,活动线程为0即将进入终态
terminated011--终结状态

从数字上比较,terminated>tidying>stop>shutdown>running

注意:这些信息存储在一个原子变量ctl中,目的是将线程池状态与线程池数量合二为一,这样可以用一次cas原子操作进行赋值,从而可以减少一次cas的原子操作

2、构造方法

  • corePoolSize 核心线程数目最多保留的线程数
  • maximumPoolSize 最大线程数目
  • keepAliveTime 生存时间针对救急线程
  • unit 时间单位针对救急线程
  • workQueue 阻塞队列
  • threadFactory 线程工厂(创建线程的可以起名字)-特定的名字后面调试就方便
  • handler 拒绝策略 

救济线程:数量就是最大线程数减去核心线程数,当我们任务来的时候先用核心线程,用完了就进入阻塞队列(前提是有界队列),当队列满了会看看有没有救济线程有了就用救济线程,没有才拒绝策略。当救济线程使用完毕后会看生存时间,当超过生存时间还没有人用到就会销毁,但核心线程不会。

拒绝策略:如果线程阻塞队列满且没救济线程就会拒绝策略,jdk提供了4种实现,其他著名的框架也提供了实现

  • AbortPolicy 让调用者抛出RejectedExecutionException异常,这是默认策略
  • CallerRunsPolicy 让调用者运行任务
  • DiscardPolicy 放弃本次任务
  • DiscardOldestPolicy 放弃队列中最早的任务,本任务取代之
  • Dubbo的实现,在抛出RejectedExecutionException异常之前会记录日志,并dump线程栈信息,方便定位问题
  • Netty的实现,创建一个新线程来执行任务(不太好,达不到限制线程的目的)
  • activeMQ的实现,带超时时间等待60s尝试放入队列,类似我们之前自定义的拒绝策略
  • PinPoint的实现,他使用了拒绝策略链,会逐一尝试策略链中每种拒绝策略

3、newFixedThreadPool

特点:

  • 核心线程数==最大线程数没救济线程被创建)因此也无需超时时间
  • 阻塞队列是无界的,可以放任意数量的任务

评价:适用于任务量已知,相对耗时的任务

4、newCachedThreadPool

带缓冲功能的线程池

特点:

核心线程数是0最大线程数是Integer.MAX_VALUE,救急线程的空闲生存时间是60s,意味着全部都是救济线程(60s后可以回收),救急线程可以无限创建

队列采用synchronousQueue实现特点是,他没有容量,没有线程来取是放不进去的(一手交钱,一手交货)

评价:整个线程池表现为线程数会根据任务量不断增加,没有上线,当任务执行完毕,空闲1分钟后释放线程,适合任务数比较密集,但每个任务执行时间比较短的情况。

5、newSingleThreadExecutor

使用场景:

希望多个任务队列排队执行,线程数固定位1,任务多于1就会进入无界队列阻塞,任务执行完毕,这是唯一的线程也不会被释放。

区别:

  • 自己创建一个单线程:串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证线程池的正常工作。
  • 固定大小线程池初始化为1:以后还可以修改,对外暴露的是ThreadPoolExecutor对象,可以强转后调用setCorePoolSize等方法进行修改。
  • 单例线程池:线程个数始终为1,不能修改,FinalizableDelegatedExectorService应用的是装饰器模式,多用FinalizableDelegatedExectorService做了一个装饰封装,只对外暴露了ExecutorService接口,因此不能调用ThreadPoolExecuotr中特有的方法

6、提交任务

execute:传入runnbale,不需要返回值,最基础的执行

submit:这种传入的是callable返回的是futrue对象,利用的是保护性暂停模式,这种就是主线程会阻塞等待futue的响应结果,有就返回。

invokeAll:接收一个callable任务的集合,返回的也是一个futrue的集合;还有一种接收3个参数的构造器,就是多个超时时间单位。主线程会阻塞等待集合全部任务全部执行完成

invokeAny:接收一个callable集合,然后他不会全部执行完成,当第一个最快执行完成的执行完了就返回,其他任务取消,所以返回的只是一个object,不是上面的list了。之前那个从多个接口获取结果,拿最快的那个应该可以用这种。

7、关闭线程池

shutdown(),线程池的状态会变成shutDown不会接收新的任务,但是已经提交的任务会执行完,此方法不会阻塞等那些要运行完的运行完,而是直接掉用shotdown就返回

showdownNow(),线程池状态会变为stop不会接收新任务,会将任务队列中的任务返回,并用interrupt的方式中断正在运行的任务

 

三、异步模式之工作线程

1、定义

让有限的工作线程worker thread来轮流异步处理无限多的任务,也可以将其归类为分工模式,他的典型实现就是线程池,也体现了经典的设计模式中的享元模式

例如,海底捞的服务器,轮流处理每位客人点餐,如果每个客人都分配一个服务员,成本就太高了

注意:不同的任务类型应该使用不同的线程池,这样能避免饥饿,并提升效率

例如:如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务B)显然效率不咋样,分成服务员(线程池A)与厨师(线程池B)更为合理

2、饥饿

固定大小的线程池会有饥饿现象

两个工人是同一个线程池中的两个线程,他们要做的事情是:为客人点餐和后厨做菜,这是两个阶段

  • 客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待
  • 后厨做菜:没啥说的,做就是了

比如工人A处理点餐任务,接下来他要等工人B把菜做好,然后上菜,他们配合的蛮好,但现在同时来两个客人,这个时候A和B都去处理点餐了,没人做菜了,这个时候就饥饿

3、解决

应该把不同的任务交给不同的线程池去处理,这样就不会导致这种都做一个任务另一个做不了导致的饥饿问题。

四、多少线程数合适(重点)

过小会导致不能充分利用系统资源、容易饥饿

过大会导致更多线程上下文切换,占用更多内存

1、CPU密集型运算

这种的瓶颈往往在于线程上下文切换,所以我们尽可能避免他。通常采用 CPU核心数+1 能够实现最优CPU利用率,+1是保证线程由于页缺失故障(操作系统)或其他原因导致暂停,额外的这个线程可以顶上去,保证CPU时钟周期不被浪费

2、I/O密集型运算

CPU不总处于繁忙状态,例如当执行业务计算时,这个时候会用cpu资源,但当执行IO操作时,远程RPC调用时,包括进行数据库操作时,这时候CPU就闲下来了,就可以利用多线程提高他的利用率,经验公式如下:

线程数 = 核数 * 期望CPU利用率 * 总时间(CPU计算时间+等待时间)/ CPU计算时间

五、任务调度线程池

1、Timer类

在任务调度线程池加入前,可以用Timer类来实现定时功能,Timer的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务

2、ScheduledThreadPoolExecutor

用这种线程池在只有1个线程的时候,当第一个任务异常是不会影响到第二个任务的。

他除了延迟执行外还可以定时执行任务,每隔一段时间执行一次,用的是scheduleAtFiexedRate方法,构造器是任务、初始延迟、间隔时间、时间单位。但是如果任务本来的执行时间就很长大于间隔时间的话,他的下一个任务不会间隔了变成仅挨着执行,可以保证任务不会重叠,但是没有间隔了。

还有个scheduleWithFixedDelay()中间的参数是每个任务的间隔时间,这个才是正在的定时间隔的任务,他的当任务超过了间隔时间了,还是会间隔固定时间才会执行下一个,他的delay是从上一个任务的结束时间开始算的。

线程池中处理异常的两种方式:

  1. try:直接用try catch手动去抓异常
  2. 用submit方式拿到future对象:主线程去调用future的get方法获取如果成功就会返回值,如果中间出现了异常会返回异常信息

3、定时任务

主要就是用LocalDataTime的时候算出目标的执行时间,用那个时间减去现在的时间就是距离开始的时间(第一个参数),第二个参数就间隔的时间,然后单位。

六、Tomcat线程池

1、介绍

Tomcat分为连接器和容器部分:

  • 连接器就是为了连接,这里就用到了线程池(我们就讲连接器的线程池)
  • 容器部分可以实现servlet规范,运行组件

  • LimitLatch 用来限流,可以控制最大连接个数,类似JUC中的Semaphore
  • Accept 只负责接收新的socket连接(是个线程不断循环接受连接)
  • Poller 只负责监听socket channel是否有可读IO事件(也是个线程负责不断循环处理是否有可读IO事件发生)一旦可读,封装一个任务对象socketProcessor,提交给Executor线程池处理
  • Executor 线程池中的工作线程最终负责处理请求

tomcat线程池扩展了原本的ThreadPoolExecutor,行为略微改了一带你,他重现写了一下那个execute方法,如果总线程数超过最大线程数,不会立刻拒绝策略抛出异常,而是再次尝试,如果还是失败才是抛出异常。

源码:他是直接用原本的方法先,如果抛出异常了说明超过最大线程数的拒绝策略了,他直接catch住然后重新尝试加入阻塞队列,如果没法加入自己再抛出 

2、参数

默认是只有Connector来配置的,但是我们也可以去配置文件注解打开Executor如果有的话,就Executor为准

这个是反过来的,当提交任务大于核心线程就会判断是否小于最大线程,如果小于最大线程是用救济线程的,超过才加入阻塞队列,然后队列满是Integer最大值,接近无界队列。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卒获有所闻

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值