Java并发包
线程池的优点:
线程池主要解决两个问题:在执行异步任务时,如果不使用线程池时就需要new 一个线程来执行,而线程的创建与销毁是需要开销的。通过线程池可以复用里面的线程,不需要每次执行新任务时都重新创建和销毁线程。此外线程池还提供了一种资源限制和管理的手段,比如限制线程个数,动态增加线程等。甚至有些线程池还提供了一些数据的统计功能。
ThreadPoolExecutor 类结构图
线程池状态含义:
● RUNNING(-1):接受新任务并且处理阻塞队列里的任务。显式调用shutdown()方法,或者隐式调用了finalize()方法里面的shutdown()方法会进入SHUTDOWN状态; 式调用shutdownNow()方法时会进入stop状态
● SHUTDOWN(0):拒绝新任务但是处理阻塞队列里的任务。队列为空并且线程池中执行的任务也为空,会进入TIDYING状态。
● STOP(-1):拒绝新任务并且抛弃阻塞队列里的任务,同时会中断正在处理的任务。线程池中执行的任务为空,会进入TIDYING状态。
● TIDYING:所有任务都执行完(包含阻塞队列里面的任务)后当前线程池活动线程数为0,将要调用terminated方法。
● TERMINATED:终止状态。terminated方法调用完成以后的状态。
状态转变:
● RUNNING -> SHUTDOWN :显式调用shutdown()方法,或者隐式调用了finalize()方法里面的shutdown()方法。
● RUNNING或SHUTDOWN)-> STOP :显式调用shutdownNow()方法时。
● SHUTDOWN -> TIDYING :当线程池和任务队列都为空时。
● STOP -> TIDYING :当线程池为空时。
● TIDYING -> TERMINATED:当terminated()hook方法执行完成时。
线程池参数定义:
● corePoolSize:线程池核心线程个数。
● maximunPoolSize:线程池最大线程数量。
● keeyAliveTime:存活时间。如果当前线程池中的线程数量比核心线程数量多,并且是闲置状态,则这些闲置的线程能存活的最大时间。
● TimeUnit:存活时间的时间单位
● workQueue:用于保存等待执行的任务的阻塞队列,比如基于数组的有界ArrayBlockingQueue、基于链表的无界LinkedBlockingQueue、最多只有一个元素的同步队列SynchronousQueue及优先级队列PriorityBlockingQueue等。
● ThreadFactory:创建线程的工厂。
● RejectedExecutionHandler:饱和策略,当队列满并且线程个数达到maximunPoolSize后采取的策略,比如AbortPolicy(抛出异常)、CallerRunsPolicy(使用调用者所在线程来运行任务)、DiscardOldestPolicy(调用poll丢弃一个任务,执行当前任务)及DiscardPolicy(默默丢弃,不抛出异常)
线程池实现的主要方式:
1 CachedThreadPool, 可以缓存的线程池,对最大线程数没有限制,可以达到Integer.MAX_VALUE, 线程存活时间是60秒, 阻塞队列使用的是SynchronousQueue这种不能存储任何元素的阻塞队列,也就是每提交一个任务,都要分配一个工作线程来处理。
2 FixedThreadPool, 固定线程数量的线程池,核心线程与最大线程是一个固定的值,二者相等。
3 SingleThreadExecutor, 只有一个工作线程的线程池。线程数量无法动态变更,可保证任务按FIFO的方式顺序执行。
4 ScheduleThreadPool,具有延迟执行功能的线程池,可以用来实现定时调度
5 WorkStealingPool, Java8新加入的线程池,在内部构造一个ForkJoinPool,利用工作窃取的算法并行处理请求。
线程执行流程:
-
1 提交一个任务,线程池里存活的核心线程数小于线程数corePoolSize时,线程池会创建一个核心线程去处理提交的任务。
-
2 如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行。
-
3 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建一个非核心线程执行提交的任务。
-
4 如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理。
JDK提供了四种拒绝策略处理类
-
AbortPolicy(抛出一个异常,默认的)
-
DiscardPolicy(直接丢弃任务)
-
DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
-
CallerRunsPolicy(交给线程池调用所在的线程进行处理)Worker继承AQS和Runnable接口,是具体承载任务的对象。Worker继承了AQS,自己实现了简单不可重入独占锁,
其中state=0表示锁未被获取状态,state=1表示锁已经被获取的状态,state=-1是创建Worker时默认的状态,创建时状态设置为-1是为了避免该线程在运行runWorker()方法前被中断。其中变量firstTask记录该工作线程执行的第一个任务,thread是具体执行任务的线程。
疑问:
1 为什么需要核心线程数和最大线程数?
一般工作业务有忙和不忙的时候,如果采取同样的线程池大小,如果线程池太小就在业务忙的时候处理业务的速度跟不上,如果太大,在业务不忙的时候又太占用资源。
2 为什么要有任务队列?
第一为核心线程转变为最大线程提供一个缓冲,不至于任务一变多就开始创建线程,创建线程是损耗的,而且需要获取全局锁。第二,为了提供线程处理不过来的时候减少业务的压力,此外也是考虑积压太多任务导致OOM。
3 为什么需要第一队列满+核心线程数满,才创建新的线程,而不是积压到一定的阙值后就创建新的线程?
这个主要是设计实现考虑的,其实可以实现积压到阙值后就创建新的线程,只是这样在计算队列的阙值时可能就耗时,比如arrayBlockingQueue, 计算size时需要获取锁,offer入队列也需要获取锁。 而实现最大值释放就只需调用offer方法即可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!