线程池之深入理解ThreadPoolExecutor

目录

1.使用线程池的原因

2.线程池的原理

3.线程池的工作流程

4.ThreadPoolExecutor类及其参数

5.源码分析

6.线程池的关闭

7.四种常见的线程池及使用场景

8.线程池的合理使用和配置

9.自定义线程池

 

一.使用线程池的原因

1、降低资源消耗

可以重复利用已创建的线程,降低线程创建和销毁造成的消耗。

2、提高响应速度

当任务到达时,任务可以不需要等到线程创建就能立即执行。

3、提高线程的可管理性

线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

 

二.线程池的原理

提交一个任务到线程池中,线程池的处理流程如下:

1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。

2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。

3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

2.1 Executor框架类图

接口:Executor, ExecutorService, ScheduledExecutorService

抽象类: AbstractExecutorService

实现类: ThreadPoolExecutor, ScheduledThreadPoolExecutor

2.1 ThreadPoolExecutor

ThreadPoolExecutor继承于AbstractExecutorService这一抽象类,AbstractExecutorService实现ExecutorService接口,ExecutorService接口继承Executor接口,Executor接口为顶层接口。

ThreadPoolExecutor中有四个构造方法:

2.3 ThreadPoolExecutor构造方法的参数

corePoolSize:线程池核心线程数量。在创建线程池后,默认情况下线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。除非调用prestartAllCoreThreads( )或prestartCoreThread( )方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程;

maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;

keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:

TimeUnit.DAYS;               //天                TimeUnit.HOURS;             //小时

TimeUnit.MINUTES;           //分钟        TimeUnit.SECONDS;           //秒

TimeUnit.MILLISECONDS;      //毫秒    TimeUnit.MICROSECONDS;      //微妙

TimeUnit.NANOSECONDS;       //纳秒

workQueue:存放任务的队列。当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能。 workQueue的类型为BlockingQueue<Runnable>,通常可以取下面三种类型:

1)有界任务队列ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;

2)无界任务队列LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;

3)直接提交队列synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

threadFactory:线程工厂,主要用来创建线程,ThreadFactory是一个接口,只有一个方法。通过线程工厂可以对线程的一些属性进行定制。  

public interface ThreadFactory {

Thread newThread(Runnable r);  

}

RejectedExecutionHandler:饱和策略,表示当拒绝处理任务时的策略,有以下四种取值:

(1) AbortPolicy  为java线程池默认的阻塞策略,不执行此任务,而且直接抛出一个运行时异常,切记ThreadPoolExecutor.execute 需要try catch,否则程序会直接退出。

(2) DiscardPolicy 直接抛弃,任务不执行,空方法,也不抛出异常。

(3) DiscardOldestPolicy 如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)。

(4) CallerRunsPolicy 线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。

 

三.源码分析

ThreadPoolExecutor源码中最核心的execute方法,这个方法在AbstractExecutorService中并没有实现,从Executor接口,直到ThreadPoolExecutor才实现了该方法。 首先来了解下ThreadPoolExecutor这个类中的参数:

1. ctl主要用于存储线程池的工作状态以及池中正在运行的线程数。显然要在一个整型变量存储两个数据,只能将其一分为二。其中高3bit用于存储线程池的状态,低位的29bit用于存储正在运行的线程数。

2. 线程池具有以下五种状态,当创建一个线程池时初始化状态为RUNNING。

RUNNING:-1<<COUNT_BITS,即高3位为1,低29位为0。允许提交并处理任务

SHUTDOWN:0<<COUNT_BITS,即高3位为0,低29位为0。不允许提交新的任务,但是会处理完已提交的任务

STOP:1<<COUNT_BITS,即高3位为001,低29位为0。不允许提交新的任务,也不会处理阻塞队列中未执行的任务,并设置正在执行的线程的中断标志位

TIDYING:2<<COUNT_BITS,即高3位为010,低29位为0。所有任务执行完毕,池中工作的线程数为0。

TERMINATED:3<<COUNT_BITS,即高3位为011,低29位为0。线程池彻底终止,就变成TERMINATED状态。

3. 调用线程池的shutdown方法,将线程池由RUNNING(运行状态)转换为SHUTDOWN状态。  调用线程池的shutdownNow方法,将线程池由RUNNING或SHUTDOWN状态转换为STOP状态。  SHUTDOWN状态和STOP状态先会转变为TIDYING状态,最终都会变为TERMINATED.

4. static int runStateOf(int c)

static int workerCountOf(int c)

static int ctlOf(int rs,int wc)

ThreadPoolExecutor同时提供上述三个方法用于池中的线程查看线程池的状态和计算正在运行的线程数。

5. completedTaskCount:表示线程池已完成的任务数。

allowCoreThreadTimeeOut:表示是否允许核心线程在空闲状态下自行销毁。  

largestPoolSize: 表示线程池从创建到现在,池中线程的最大数量

1、如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。

2、如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。

3、如果无法将任务加入BlockingQueue(队列已满),则在非corePool中创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。

4、如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution()方法。 ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。

addWorker()

addWorker(Runnable firstTask, boolean core)的主要任务是创建并启动线程。 它会根据当前线程的状态和给定的值(core or maximum)来判断是否可以创建一个线程。

执行流程: 1、判断线程池当前是否为可以添加worker线程的状态,可以则继续下一步,不可以return false:    

A、线程池状态>shutdown,可能为stop、tidying、terminated,不能添加worker线程    

B、线程池状态==shutdown,firstTask不为空,不能添加worker线程,因为shutdown状态的线程池不接收新 任务    

C、线程池状态==shutdown,firstTask==null,workQueue为空,不能添加worker线程,因为firstTask为空是 为了添加一个没有任务的线程再从workQueue获取task,而workQueue为空,说明添加无任务线程已经没有 意义

2、线程池当前线程数量是否超过上限(corePoolSize 或 maximumPoolSize),超过了return false,没超过则 对workerCount+1,继续下一步

3、在线程池的ReentrantLock保证下,向Workers Set中添加新创建的worker实例,添加完成后解锁,并启动 worker线程,如果这一切都成功了,return true,如果添加worker入Set失败或启动失败,调用 addWorkerFailed()逻辑。

总结:源码比较长,其实就做了两件事。1)用循环CAS操作来将线程数加1;2)新建一个线程并启用

addWorker共有四种传参方式。execute使用了其中三种,分别为:

1.addWorker(paramRunnable, true)

线程数小于corePoolSize时,放一个需要处理的task进Workers Set。如果Workers Set长度超过 corePoolSize,就返回false. 2.addWorker(null, false)

放入一个空的task进workers Set,长度限制是maximumPoolSize。这样一个task为空的worker 在线程执行的时候会去任务队列里拿任务,这样就相当于创建了一个新的线程,只是没有马上分配任务。

3.addWorker(paramRunnable, false)

当队列被放满时,就尝试将这个新来的task直接放入Workers Set,而此时Workers Set的长度 限制是maximumPoolSize。如果线程池也满了的话就返回false.

还有一种情况是execute()方法没有使用的 addWorker(null, true) 这个方法就是放一个null的task进Workers Set,而且是在小于corePoolSize时,如果此时Set中 的数量已经达到corePoolSize那就返回false,什么也不干。实际使用中是在 prestartAllCoreThreads()方法,这个方法用来为线程池预先启动corePoolSize个worker等待从 workQueue中获取任务执行。

 

runWoker():

runWorker的执行流程:

1、Worker线程启动后,通过Worker类的run()方法调用runWorker(this) 。

2、执行任务之前,首先worker.unlock(),将AQS的state置为0,允许中断当前worker线程 。

3、开始执行firstTask,调用task.run(),在执行任务前会上锁wroker.lock(),在执行完任务后会解锁,为了防止在任务运行时被线程池一些中断操作中断 。

4、在任务执行前后,可以根据业务场景自定义beforeExecute() 和 afterExecute()方法 。

5、无论在beforeExecute()、task.run()、afterExecute()发生异常上抛,都会导致worker线程终止,进入processWorkerExit()处理worker退出的流程 。

6、如正常执行完当前task后,会通过getTask()从阻塞队列中获取新任务,当队列中没有任务,且获取任务超时,那么当前worker也会进入退出流程。

 

getTask():

可以看出,如果允许线程在keepAliveTime时间内未获取到任务线程就销毁就调用workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS),否则会调用workQueue.take()方法(该方法即使获取不到任务就会一直阻塞下去)。而确定是否使用workQueue.poll方法只有两个条件决定,

一个是当前池中的线程是否大于核心线程数量,

第二个是是否允许核心线程销毁,两者其一满足就会调用该方法。

shutdown():

a. 开始一个有序的关闭,在关闭中,之前提交的任务会被执行(包含正在执行的,在阻塞队列中的),但新任务会被拒绝。

b.  如果线程池已经shutdown,调用此方法不会有附加效应. c. 当前方法不会等待之前提交的任务执行结束,可以使用awaitTermination()。

shutdownnow():

a. 尝试停止所有活动的正在执行的任务,停止等待任务的处理,并返回正在等待被执行的任务列表,这个任务列表是从任务队列中排出(删除)的。

b. 这个方法不用等到正在执行的任务结束,要等待线程池终止可使用awaitTermination()。

shutdownNow() 和 shutdown()的大体流程相似,差别是:

1、将线程池更新为stop状态。

2、调用 interruptWorkers() 中断所有线程,包括正在运行的线程。

3、将workQueue中待处理的任务移到一个List中,并在方法最后返回,说明shutdownNow()后不会再处理workQueue中的任务。

awaitTermination()

参数:    

timeout:超时时间    

unit:     timeout超时时间的单位

返回:     true:线程池终止     false:超过timeout指定时间 在发出一个shutdown请求后,在以下3种情况发生之前,awaitTermination()都会被阻塞 1、所有任务完成执行 2、到达超时时间 3、当前线程被中断

四.四种常见的线程池

newFixedThreadPool:

1)FixedThreadPool 的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超过核心线程数后,任务都会被放到阻塞队列中。

2)此外 keepAliveTime 为 0,也就是多余的空余线程会被立即终止(由于这里没有多余线程,这个参数也没什么意义了)。

3)而这里选用的阻塞队列是 LinkedBlockingQueue,使用的是默认容量 Integer.MAX_VALUE,相当于没有上限。 因此这个线程池执行任务的流程如下:

a. 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务

b. 线程数等于核心线程数后,将任务加入阻塞队列

c. 由于队列容量非常大,可以一直加入

d. 执行完任务的线程反复去队列中取任务执行 FixedThreadPool 用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。

newCachedThreadPool:

可以看到,CachedThreadPool 没有核心线程,非核心线程数无上限,但是每个线程空闲的时间只有 60 秒,超过后就会被回收。

CachedThreadPool 使用的队列是 SynchronousQueue,这个队列的作用就是传递任务,并不会保存。 因此当提交任务的速度大于处理任务的速度时,每次提交一个任务,就会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。

它的执行流程如下:

a. 没有核心线程,直接向 SynchronousQueue 中提交任务。

b. 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个。

c. 执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就死亡 。

d. 由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。 CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器。

newSingleThreadExecutor:

从参数可以看出来,SingleThreadExecutor 相当于特殊的 FixedThreadPool,它的执行流程如下:

a. 线程池中没有线程时,新建一个线程执行任务

b. 有一个线程以后,将任务加入阻塞队列,不停加入

c. 唯一的这一个线程不停地去队列里取任务执行 SingleThreadExecutor 用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。

newScheduledThreadPool:

定时线程池,该线程池可用于周期性地去执行任务,通常用于周期性的同步数据。

执行方法: scheduleAtFixedRate:是以固定的频率去执行任务,周期是指每次执行任务成功执行之间的间隔。

schedultWithFixedDelay:是以固定的延时去执行任务,延时是指上一次执行成功之后和下一次开始执行的之前的时间。

五.线程池的合理配置

如何选择线程池数量: 线程池的大小决定着系统的性能,过大或者过小的线程池数量都无法发挥最优的系统性能。

当然线程池的大小也不需要做的太过于精确,只需要避免过大和过小的情况。一般来说,确定线程池的大小需要考虑CPU的数量,内存大小,任务是计算密集型还是IO密集型等因素

NCPU = CPU的数量

UCPU = 期望对CPU的使用率 0 ≤ UCPU ≤ 1

W/C = 等待时间与计算时间的比率

如果希望处理器达到理想的使用率,那么线程池的最优大小为:

线程池大小=NCPU *UCPU(1+W/C)

在Java中使用

int ncpus = Runtime.getRuntime().availableProcessors();

获取CPU的数量。

六.自定义线程池

首先定义好参数,如核心线程池的数量,线程池大小,生存时间。

然后改写线程工厂这个方法。

然后我们就定义好一个简单的线程池。

 

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值