【java学习】ThreadPoolExecutor 线程池

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SunshineTan/article/details/82876128

1,概念

一个线程池管理了一组工作线程,同时它还包括了一个用于放置等待执行任务的任务队列(阻塞队列)。
默认情况下,在创建了线程池后,线程池中的线程数为0。

Android中的线程池来自Java,主要通过Executor来派生特定类型的线程池。
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer 类。

1.1 Executor接口

Executor 是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable 类型,从字面意思可以理解,就是用来执行传进去的任务的;
真正的线程池的实现为ThreadPoolExecutor。

1.2 Executors 类

1.3 ExecutorService 接口

继承了Executor 接口,并声明了一些方法:submit、invokeAll、invokeAny 以及shutDown 等;

1.4 AbstractExecutorService 抽象类

实现了ExecutorService 接口,基本实现了ExecutorService 中声明的所有方法;

1.5 ThreadPoolExecutor 类

继承了类AbstractExecutorService。

2,优点

①重用线程池中的线程
避免因为线程的创建和销毁所带来的性能开销。
②有效控制线程池的最大并发数
避免大量的线程之间因互相抢占系统资源而导致的阻塞现象。
如果不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存。
③提高线程的可管理性。
使用线程池可以对线程进行统一的分配和监控,提供定时执行、指定间隔循环执行等功能。
④提高响应速度。
当任务到达时,任务可以不需要等到线程创建就可以立即执行。

3,参数配置

ThreadPoolExecutor 的构造方法提供了一系列参数来配置线程池。

public ThreadPoolExecutor(int corePoolSize,
						  int maximumPoolSize,
						  long keepAliveTime,
						  TimeUnit unit,
						  BlockingQueue<Runnable> workQueue,
						  ThreadFactory threadFactory)					

–corePoolSize
线程池的核心线程数。
默认情况下,核心线程在线程池中一直存活,即使处于闲置状态。
通过设置ThreadPoolExecutor的allowCoreThreadTimeOut属性为true,可以让闲置的核心线程在等待新任务时有超时策略。这个时间间隔由keepAliveTime所指定,超时后,线程终止。

–maximumPoolSize
线程池所能容纳的最大线程数。
当活动线程数达到此值,后续新任务将会被阻塞。

–keepAliveTime
非核心线程闲置时的超时时长。
超过此时长,非核心线程就会被回收。
当ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true时,可作用于核心线程。

–unit
用于指定keepAliveTime参数的时间单位,这是一个枚举,常用的有TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分钟)。

–workQueue
线程池中的任务队列,通过线程池的execute方法提交的Runnable对象会存储在这个参数中。

–threadFactory
线程工厂,为线程池提供创建新线程的功能。
ThreadFactory是一个接口,它只有一个方法:Thread newThread(Runnable r)。

–RejectedExecutionHandler handler
这个参数不常用。
当线程无法执行新任务时,可能原因:由于任务队列已满或者是无法成果执行任务。这个时候ThreadPoolExecutor会调用handler的rejectedExecution方法来通知调用者。

4,处理策略(核心线程corePoolSize 、任务队列workQueue、最大线程maximumPoolSize

当任务提交给线程池之后的处理策略如下:

  1. 如果此时线程池中的数量小于corePoolSize(核心池的大小),即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务(也就是每来一个任务,就要创建一个线程来执行任务)。
  2. 如果此时线程池中的数量大于等于corePoolSize,但是缓冲队列workQueue 未满,那么任务被放入缓冲队列,则该任务会等待空闲线程将其取出去执行。
  3. 如果此时线程池中的数量大于等于corePoolSize , 缓冲队列workQueue 满,并且线程池中的数量小于maximumPoolSize(线程池最大线程数),建新的线程来处理被添加的任务。
  4. 如果此时线程池中的数量大于等于corePoolSize , 缓冲队列workQueue 满,并且线程池中的数量等于maximumPoolSize,那么通过RejectedExecutionHandler 所指定的策略(任务拒绝策略)来处理此任务。也就是处理任务的优先级为: 核心线程corePoolSize 、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler 处理被拒绝的任务。
  5. 特别注意,在corePoolSize 和maximumPoolSize 之间的线程数会被自动释放。当线程池中线程数量大于corePoolSize 时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize。这样,线程池可以动态的调整池中的线程数。

5,分类

AsyncTask对THREAD_POOL_EXECUTOR这个线程池进行了配置。

Android中最常见的4类线程池,都是直接或间接地通过配置ThreadPoolExecutor来实现自己的功能特性。

5.1 FixedThreadPool(固定线程池)

通过Executors的newFixedThreadPool方法来创建。
是一种线程数量固定的线程池,不回收空闲线程,除非线程池关闭了。

FixedThreadPool只有核心线程并且这些核心线程不会被回收。—>能够快速相应外界请求。

创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

-newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程
-其独特之处:任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子
-从方法的源代码看,cache池和fixed 池调用的是同一个底层池,只不过参数不同:

ExecutorService pool = Executors.newFixedThreadPool(2); //核心线程数为2
  //创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口 
  Thread t1 = new MyThread(); 
   t1.setPriority(1);//设置线程优先级
  Thread t2 = new MyThread(); 
  Thread t3 = new MyThread(); 
  Thread t4 = new MyThread(); 
  Thread t5 = new MyThread(); 
  //将线程放入池中进行执行 
  pool.execute(t1); 
  pool.execute(t2); 
  pool.execute(t3); 
  pool.execute(t4); 
  pool.execute(t5); 

5.2 CachedThreadPool(缓存型池子)

通过Executors的newCachedThreadPool方法来创建。
是一种线程数量不定的线程池,只有核心线程且最大线程数为Interger.MAX_VALUE(很大,相当于线程数可以任意大)。

当线程池中没有空闲线程时,线程池会创建新的线程来处理新任务。
对于空闲线程有超时机制,超过60s闲置线程会被回收。注意,放入CachedThreadPool的线程不必担心其结束,超过TIMEOUT不活动,其会自动被终止。
调用 execute 将重用以前构造的线程(如果线程可用),如果现有线程没有可用的,则创建一个新线程并添加到池中。

场景:适合执行大量的耗时较少的任务(即生存期较短的异步型任务)。(大量线程处于空闲状态,会停止,几乎不占系统资源)

5.3 ScheduledThreadPool(调度型线程池)

通过Executors的newScheduledThreadPool方法来创建。
核心线程数量固定,非核心线程数量无限制且闲置时会立即回收。
这个池子里的线程可以按schedule依次delay执行,或周期执行。

场景:用于执行定时任务和具有固定周期的重复任务。

5.4 SingleThreadExecutor(单例线程)

通过Executors的newSingleThreadPool方法来创建。
线程池内部只有一个核心线程,它确保所有的任务都在同一个线程中按顺序执行。
以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。

创建一个使用单个 worker 线程的 Executor,
可保证顺序地执行各个任务,任意时间池中只能有一个线程。与其他等效的 newFixedThreadPool(1) 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。
用的是和cache池和fixed池相同的底层池,但线程数目是1-1,0秒IDLE(无IDLE)

优点:统一所有的外界任务到一个线程中,这使得在这些任务之间不需要处理线程同步的问题。

6,风险

虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。
①线程池的大小。
多线程应用并非线程越多越好,需要根据系统运行的软硬件环境以及应用本身的特点决定线程池的大小。一般来说,如果代码结构合理的话,线程数目与CPU
数量相适合即可。如果线程运行时可能出现阻塞现象,可相应增加池的大小;如有必要可采用自适应算法来动态调整线程池的大小,以提高CPU 的有效利用率和系统的整体性能。
②并发错误。
多线程应用要特别注意并发错误,要从逻辑上保证程序的正确性,注意避免死锁现象的发生。
③线程泄漏。
这是线程池应用中一个严重的问题,当任务执行完毕而线程没能返回池中就会发生线程泄漏现象。

7,常用方法

ThreadPoolExecutor 类常用方法:

7.1 execute()

启动线程池。有返回值。

7.2 submit()

启动线程池。有返回值。
submit 方便异常的处理。如果任务可能会抛出异常,而且希望外面的调用者能够感知这些异常,那么就需要调用submit 方法,通过捕获Future.get抛出的异常。

7.3 shutdown()

关闭线程池。
此方法执行后不得向线程池再提交任务,如果有空闲线程则销毁空闲线程,等待所有正在执行的任务及位于阻塞队列中的任务执行结束,然后销毁所有线程。

7.4 shutdownNow()

关闭线程池。
此方法执行后不得向线程池再提交任务,如果有空闲线程则销毁空闲线程,取消所有位于阻塞队列中的任务,并将其放入List<Runnable>容器,作为返回值。取消正在执行的线程(实际上仅仅是设置正在执行线程的中断标志位,调用线程的interrupt 方法来中断线程)。

8,线程池配置

8.1 CPU 密集型任务(CPU核心数+1)

可以少配置线程数,大概和机器的cpu 核数相当,可以使得每个线程都在执行任务。
因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。

8.2 IO 密集型任务(2*CPU核心数)

由于需要等待IO 操作,线程并不是一直在执行任务,则配置尽可能多的线程,2*cpu 核数。
IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。

8.3 有界队列和无界队列的配置

需区分业务场景,一般情况下配置有界队列,在一些可能会有爆发性增长的情况下使用无界队列。

8.4 任务非常多时

使用非阻塞队列使用CAS 操作替代锁可以获得好的吞吐量。synchronousQueue 吞吐率最高。

9,性能优化

1)避免创建过多的对象
2)不要过多使用枚举
枚举占用的内存空间比整型大。
3)常量请使用static final来修饰
4)使用一些Android特有的有更好性能的数据结构
如SparseArray、Pair等。
5)适当使用软引用和弱引用
6)采用内存缓存和磁盘缓存
7)尽量采用静态内部类
避免由于内部类OOM。

猜你喜欢

转载自blog.csdn.net/SunshineTan/article/details/82876128