首先要知道线程池是用来干嘛的,线程池是通过复用线程达到最大利用资源的。
线程池的关键参数
线程池的构造方法如下
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
简单解释一下:
corePoolSize : 核心线程数,默认情况下不会退出;
maximumPoolSize: 最大线程数,这个数字是包括核心线程数的,最大线程数 = 核心线程数+非核心线程数;
long keepAliveTime:线程保持存活的时间,核心线程也可以设置,但是默认情况下不会退出;非核心线程数这个参数默认生效;
TimeUnit unit :keepAliveTime 的时间单位,时分秒等;
workQueue : 工作队列,是个阻塞队列;
threadFactory : 线程工厂,通常用来设置线程的名字和优先级等;
handler :当线程池处理不了提交的任务时,需要告诉线程池要怎么处理,例如抛异常等。
线程池模型
网上有很多讲解线程池的图片、代码讲解,但是看起来依然很费力,我把源码大体线程复用流程写了个伪代码(核心线程不停的默认情况),可以对比源码理思路,这里不贴了:
void execute(Runnable r){
if(当前线程数<core ){
New Thread(new Runnable(){
Public void run(){
While(r!=null || r = takeFromQueue()!= null){
r.run();
r = null;
}
}
})
}else if(!queue.isFull()){
queque.addTask(r);
}else if(当前线程数<max ){
New Thread(new Runnbale(){
Public void run(){
While(r != null || r = pollAllowTimeout(超时时间)){
r.run();
R = null;
}
}
})
}else{
拒绝策略;
}
}
伪代码中线程里面的操作很像,但是为了好理解分开写了。执行顺序:
1、如果有任务来了,没有到达核心线程数,会立即新建一个线程执行,并且线程生命周期不会结束(terminal状态),因为run()方法没有走完,这个任务执行完后(r = null),线程会继续从阻塞队列里拿任务,拿不到会一直阻塞(take),默认不会停。
2、超过核心数,但是阻塞队列没有满,会把任务放在阻塞队列里;
3、阻塞队列满了,但是没有达到最大核心数,会新建一个线程,立马执行这个任务,执行完后从队列取任务,但是这时候是poll,是有超时退出的取,这个超时时间就是keepAliveTime。
5、队列满了,线程数也达到最大值了,线程池不知道怎么处理,用handler的策略来拒绝任务。
线程池的原理
1、根据上面分析,核心线程默认不会停的,线程的生命周期在run()方法执行完结束,首先会在while中循环,提交的任务执行完后还会从队列取,即使队列为空,也会阻塞在那等待;–复用线程
2、核心任务(没达到核心线程数时提交的任务)是不会加到队列里的,非核心任务才会加到队列里;用队列控制吞吐,是常用的设计思想之一。–控制吞吐
再来看看我们常用的几种线程池:
CachedThreadPool :
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
核心线程数为0,最大线程数无界(Integer.MAX_VALUE),60s后非核心线程退出,使用的队列是同步队列SynchronousQueue,SynchronousQueue是个无容量队列,所以不能通过peek方法获取头部元素;也不能单独插入元素,put和take是成对出现的,put一个元素,一定要等另一端take掉。所以CachedThreadPool一旦有任务,会立即新建一个非核心线程执行,由于是线程数无上限,所以一旦任务多,就会不停地新建线程,60s后还没执行完,就会超时退出。另一个角度,任务在小于且接近60s的时间里结束,那么可能会有大量的线程在同时进行,占用CPU,这种情况总结就是**“提交速度大于任务执行速度”,所以CachedThreadPool适合提交频繁,且执行时间短的任务**。
FixedThreadPool:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
核心线程数和最大线程数是一样的,也就是只有n个核心线程,而LinkedBlockingQueue是无界的队列(容量为Integer.MAX_VALUE),也就是不会满,也就不会产生非核心线程数,第一个任务结束后,核心线程会一直从队列里取任务不退出。
SingleThreadExecutor:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
核心线程数和最大线程数都是1,是FixedThreadPool的特例(n = 1),不同的是,由于只有一个核心线程,保证了所有任务都是在一个线程里有序执行。
Android线程池和内存优化
线程池和内存有什么关系?
1、核心线程的锅。由于核心线程默认不退出的特点,核心线程指向的对象一直都是运行中的,如果核心线程开的很大,那么这些线程对象在线程池使用shutdown之前,都是占内存的。
再看下面一个例子:
Executors.newSingleThreadExecutor(new ThreadFactory() {
@Override
public Thread newThread(@NonNull final Runnable r) {
new Thread(){
@Override
public void run() {
r.run();
}
};
return null;
}
});
factory里的线程是匿名内部类,一旦这段代码放在Activity里,那么Thread对象会持有Activity的引用,而且是个强引用,那么外部的activity就一定会内存泄漏。
解决办法:
- ThreadFactory 通过声明为静态内部类;
- 如果在Activity中的线程池,有必要的话请考虑在onDestory的时候线程池shutdown。
2、阻塞队列的锅。像SingleThreadPool和FixThreadPool这两种线程池,队列大小是无界的,一旦线程处理的速度慢,而任务提交的快,就会有大量的任务堆积在队列里,从而在执行完之前造成一段时间的内存泄漏。
解决办法:合理设置线程池大小。