多线程学习笔记-深入理解ThreadPoolExecutor
java多线程中,线程池的最上层接口是Executor,ExecutorService实现了Executor,是真正的管理线程池的接口,ThreadPoolExecutor间接继承了ExecutorService,提供了多种具体的线程池实现,在日常开发中一般直接使用Executors工具类提供的几种常用ThreadPoolExecutor,下面详细介绍下ThreadPoolExecutor.
ThreadPoolExecutor基本参数
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 1000L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<Runnable>(20),
new ThreadPoolExecutor.CallerRunsPolicy());
corePoolSize:核心线程数的大小,也有说线程池最小线程数
maximumPoolSize:线程池最大线程数
keepAliveTime:当没有任务执行时,线程能存活的最大时间,这里说的线程是指大于corePoolSize,小于maximumPoolSize的线程
timeunit:keepAliveTime的时间单位
workQueue:用来存放task的堵塞队列,队列的选择和size的大小对线程池的运行有直接影响,默认有几种实现,后面详说.
rejectHandler:拒绝策略,当队列已满并且线程数达到maximumPoolSize时,再有新任务进来时会执行拒绝策略,默认集中实现,后面详说.
ThreadPool模型初始化
ThreadPool线程池初始化时,不会创建corePoolSIze的线程,也就是说在没有task进来的时候,线程池是空的,当有task进来的时候,开始创建线程,并且线程执行完task后不会销毁,而是驻留内存,直至达到corePoolSize,那什么时候线程数会再度增加达到maxPoolSize呢,这就取决于存放task的queue的size了,如果task的数量一直不超过指定的size那么就不会创建新的线程出来,反之,则会创建新的线程去执行task,那么新建出来的线程执行完task也会一直驻留内存吗?答案是不会,这时候就要看设置的keepAliveTime,如果在超过了这个时间后还是没有task去使用这个线程,则线程销毁,直至线程数等于corePoolSize.那么如果queue也满了,线程数也达到maxPoolsize,这时候怎么办呢,这时rejectHandler就会发挥作用,会根据我们指定的拒绝策略去处理这种场景.
Executors的几个默认实现
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>())); }
1.newSingleThreadExecutor:创建一个单线程的线程池.如果这个线程在执行霍城中因为异常结束,则会创建一个新的线程来代替它,这个线程池保证所有任务的执行顺序是按照任务提交顺序进行.
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()); }
2.newFixedThreadPool:创建一个固定大小的线程池.看源码可知,该实现创建了一个corePoolSize等于maximumPoolSize的线程池.
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>()); }
3.newCachedThreadPool:创建一个可缓存的线程池.看源码可知corePoolSize=0,即当线程空闲时会被回收,当线程忙碌时又可以"源源不断"的创建新线程来执行task.
默认线程池实现中还有ScheduledThreadPoolExecutor,可以实现定时的一些功能,可用来代替Timer或者TimeTask.这里不再展开说.
线程池的堵塞队列
如何选择线程池的堵塞队列,取决我们的业务场景,即我们希望以一种什么样的排队策略来处理任务.排队通常有3种策略,对应下面几种queue.
直接提交(SynchronousQueue):不排队,这个队列不会存储task,会将调用方的task直接提交给线程,指定了SynchronousQueue的线程池通常会把maximumPoolSize配置的比较大,否则可能会导致没有足够的线程来执行task,而导致task无法放入queue而被丢弃或拒绝.
有界队列(ArrayBlockingQueue):通过指定ArrayBlockingQueue的size可以设置队列的最大存储个数,当超出这个个数时就会新建线程去执行task直至线程数达到maximumPoolSize,有界队列size的设置和maximumPoolSize的设置息息相关.会影响CPU的使用率以及系统吞吐量.
无界队列(不设定size的LinkedBlockingQueue):当线程数达到corePoolSize的仍有task进来时,会源源不断进队列,由于无解,maximumPoolSize参数会失效,线程数最大只能达到corPoolSize.
线程池拒绝策略
CallerRunsPolicy:使用调用方的线程来执行task,通常情况下调用方线程就是指我们所说的主线程,这样的好处是不会丢弃task,但是缺点也很明显,使用这种策略会堵塞主线程,进而拖慢主线程的整个调用时长.而基于此,该策略同时也减缓了新的task提交进来的速度(因为主线程本来只需要用来提交task就好了,现在直接去执行task,后面的task进来的速度就慢了).
AbortPolicy:这个策略简单粗暴,直接抛出异常,不跟你多BB,需要注意的是,这个是jdk的默认策略.
DiscardPolicy:这个和AbortPolicy差不多,区别是不会抛出异常,直接丢弃.
DiscardOldestPolicy:这也是一种丢弃策略,不过和上面的DiscardPolicy刚好相反,她丢弃的不是新进来的task而是在堵塞队列中存在时间最久的那个task,即丢弃最早进入队列并且还没有被执行的task.