线程池原理深入讲解,掌握线程池看这一篇就够了

一,目的

  1. 缺陷:传统的方式是通过自己new一个线程来执行多线程任务,存在以下缺陷:
  • 当有大量任务需要执行时会导致系统中new的线程数急剧膨胀最后会耗尽系统资源,导致系统不可用
  • new线程的时候会需要频繁的进行创建和销毁线程,这会比较消耗系统资源
  1. 用线程的好处:
  • 可以管理和控制线程数,不会导致任务量大的时候不停的new线程最终耗尽系统资源
  • 可以实现线程复用,执行完任务的线程不会立即销毁而是放入线程池等待下次复用,这样避免了频繁创建销毁线程带来的资料浪费

二,类结构

  • ThreadPoolExecutor继承自AbstractExecutorService,AbstractExecutorService实现了ExecutorService接口,ExecutorService接口又继承了Executor接口

三,主要的执行逻辑

  • 加入一个任务到线程池中,先判断线程池中线程数是否到达核心线程数,如果没有到则新建一个线程来执行任务,执行任务的线程在执行完第一个任务之后会从队列中获取任务,如果队列为空则会则调用队列take方法进行阻塞
  • 如果到了,则加入阻塞队列中
  • 如果阻塞队列也满了,则判断当前线程数是否超过最大线程数如果没达到则再创建线程去执行
  • 如果到了则执行拒绝策略
线程的两种状态:
  • 运行中的线程:获取了Worker锁并正在执行run方法的线程
  • 闲置线程:因调用队列take方法被阻塞的线程,没有获取Worker锁
!!线程池构造函数参数:
  • corePoolSize:核心线程数,线程数没到这个值时创建新的线程执行任务,当有闲置线程的时候回销毁闲置线程保留线程数为该数值
  • workQueue:阻塞队列,线程数达到核心线程数之后新加入的任务加入该阻塞队列
  • maximunPoolSize:最大线程数,阻塞队列满了之后会新创建线程数到该数值,到达该数值之后就不再创建线程了,如果还有任务过来直接执行拒绝策略
  • ThreadFactory:创建线程的工程,可以通过这个自定义线程的名字,group等属性
  • RejectedExecutionHandler:自定义的饱和策略(默认是AbortPolicy 抛出异常)
  • KeyAliveTime:存活时间,当线程池中线程数超过核心线程数并且是闲置状态时最大可存活时间
  • TimeUnit:时间单位
默认提供的几个线程实现类(通过Executors工具类进行创建)
  • newFixedThreadPool:
参数:(入参threads,入参threads,0,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runable>())
特征:核心线程数和最大线程数一样,阻塞队列是无界的,存活时间为0。这意味着只要线程数达到核心线程数之后的任务都是加入阻塞队列,而且队列是无界的,如果任务量很大的话,会导致队列数据无限膨胀;线程超过核心的只要闲置立刻被回收。
应用场景:由于该线程池线程数固定,且不被回收,线程与线程池的生命周期同步,所以适用于任务量比较固定但耗时长的任务。
  • newSingleThreadExecutor
参数:(1,1,0,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runable>())
特征:核心线程数和最大线程数都是1,阻塞队列是无界的,存活时间为0。这意味着只要只有一个线程工作,而且队列是无界的,如果任务量很大的话,会导致队列数据无限膨胀;线程超过核心的只要闲置立刻被回收
应用场景:多个任务顺序执行的场景。
  • newCachedThreadPool
参数:(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue<Runable>())
特征:核心线程数为0和最大线程数是int的最大值,阻塞队列是最多只有一个任务的同步队列的,存活时间为60s。这意味着只要有任务来就会先放到队列中,如果队列有一个元素了就直接创建新线程来执行任务,如果任务量很大的话,会导致线程数无限膨胀;所有线程闲置超过60s就会被回收
应用场景:适合任务量大但耗时少的任务

四,主要属性

  1. RejectedExecutionHandler:传入的饱和策略
  2. ReentrantLock mianLock:用来控制新增Worker
  3. Set:线程中所有的线程集合(主要用于执行shutdown方法时遍历所有线程进行中断)
  4. AtomicInteger类型的ctl属性:高3位表示状态,低29位表示线程池中已有的总线程数
  5. BlockingQueue workQueue:传入的阻塞队列
  6. Condition termination:mainLock的条件类,主要用于在调用awaitTermination时存放需要阻塞的线程

五,线程池主要状态:状态存储在ctl变量的高三位

  1. RUNNING:运行状态(高三位值为-1),接受任务并消费队列中的任务
  2. SHUTDOWN:(高三位值为0),不接受任务但是消费队列中的任务
  3. STOP:(高三位值为1),不接受任务且抛弃队列中的任务且中断正在执行中的任务
  4. TIDYING:所有任务都被终止并且线程数为0(高三位值为2),所有任务执行完包(括队列中的任务之后)活动线程数为0则调用terminated方法
  5. TERMINATED:终止状态,terminated方法方法执行之后的状态
状态转换
  1. RUNNING - SHUTDOWN :调用shutdown方法
  2. RUNNING/SHUTDOWN - STOP :调用shutdownnow方法
  3. SHUTDOWN - TIDYING:当线程池和任务队列都为空时
  4. SOTP - TIDYING:当线程池为空时
  5. TIDYING - TERMINATED:当terminated hook方法执行完成时

六,主要方法

  • execute方法:先获取线程数,然后判断是否大于核心线程数,如果小于则调用addWorker方法新增线程并执行任务;否则加入阻塞队列(调用的是offer方法,如果失败不会阻塞会直接返回false),如果加入成功则再次检查线程池状态,如果不是运行状态则移除刚刚加入队列的任务并执行饱和策略,如果是运行状态则检查线程数是否为0,如果为0则新建一个空任务线程,如果加入队列失败则新增线程执行任务,如果新增失败则执行饱和策略
  • submit方法:跟execute方法比较最大的不同在于该方法有返回值,如果传入的是Runable方法也会被适配成Callable对象并固定返回传入的返回值(如果没有传默认是null),如果传入的是Callable对象,则返回值是该对象call方法执行完之后的结果。执行该方法都会返回一个Future对象,通过该对象的get方法可以获取执行结果,如果调用时方法还没执行完则线程会被LockSupport.park(this)阻塞直到有结果返回
  • addWorker方法:线程池将线程封装成Worker类,Worker类继承自AQS并实现Runable接口,这意味着Worker类本身就是一个线程,并且有自己的独占锁(Worker中实现的AQS比较简单是个不可重入的独占锁),Worker实现AQS主要的作用是:线程运行时调用当前线程实例的锁锁住自身在只要当前线程池中还有任务没有执行完就不会释放锁(Worker被start之后会死循环处理线程池中的任务,当线程池中没有可处理任务时才终止循环),当进行shutdown等操作的时候就可以通过遍历线程然后获取线程实例锁的方式来判断当前线程实例是运行状态还是闲置状态(如果能获取锁则证明当前线程处于闲置状态,如果不能获取锁则证明出于运行状态)
  • shutdown方法:中断闲置线程但不中断正在执行任务的线程,注意只是调用线程interrupt方法给出中断信号,具体怎么响应中断需要看Runnable中怎么处理
  • shutdownnow方法:中断闲置和正在执行任务的线程
  • tryTerminated方法:尝试进行线程池终止,执行流程如下:
  1. 判断线程池是否需要进入终止流程(只有当shutdown状态+workQueue是或者stop状态才需要)
  2. 判断是否还有线程,有则唤醒一个空闲线程,再次发出中断信号
  3. 如果状态是SHUTDOWN,队列空了,线程数为0,则将状态置为TIDYING,然后调用需要子类实现的terminated方法,最后状态置为terminated,并唤醒因调用awaitTerminated而进入阻塞的线程

七,问题

  1. 超过核心线程数之后如何回收线程?
    如果线程数超过核心线程在从队列中获取任务时就会调用带超时的poll方法,如果超时后仍然获取不到任务则会在下次一次循环时递减线程数并返回null,返回null之后Worker线程就会调用processWorkerExit方法将该Worker实例从集合中移除,被移除的线程没有地方引用将会被GC回收
  2. 线程池的各种使用场景?
  • 对于高并发任务执行时间短的场景,线程池的线程数可以设置为cpu核数+1,这样可以避免线程切换带来的开支
  • 对于并发不是很高且任务执行时间长的场景,根据任务执行时间长分为两种类型:

io密集型任务:指任务都停留着io阻塞上,这时候需要增加线程数以尽量让cpu忙起来提高系统吞吐量

cpu密集型任务:指任务都有大量的cpu运算,这时候只能设置线程数cpu核数+1,尽量避免线程切换带来的开支

  • 对于高并发且任务执行时间长的场景,解决这类问题的关键不是从线程池上优化了需要考虑整体架构上进行优化,第一步是看看能不能加缓存,第二步加服务器,第三部分析执行时间长的任务看看能不能通过中间件对任务进行拆分和解耦
  1. 线程池的关键参数如何设置?
    需要根据几个值来决定

tasks :每秒的任务数,假设为500~1000

taskcost:每个任务花费时间,假设为0.1s

responsetime:系统允许容忍的最大响应时间,假设为1s

做几个计算

corePoolSize = 每秒需要多少个线程处理?
threadcount = tasks/(1/taskcost) =tasks*taskcout = (500~1000)*0.1 = 50~100 个线程。corePoolSize设置应该大于50根据8020原则,如果80%的每秒任务数小于800,那么corePoolSize设置为80即可

queueCapacity = (coreSizePool/taskcost)responsetime计算可得 queueCapacity = 80/0.11 = 80。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。

maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)(最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数 计算可得 maxPoolSize = (1000-80)/10 = 92

rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理

keepAliveTime和allowCoreThreadTimeout:采用默认通常能满足

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值