一、线程池的优点:
- 重用线程池中的线程,避免因为线程的创建和销毁所带来的性能开销。
- 能有效控制线程池的最大并发数量,避免大量线程间因相互抢占系统资源而导致阻塞。
- 能够对线程进行简单的管理,提供定时执行、指定间隔循环执行等功能。
二、ThreadPoolExecutor:
- TreadPoolExecutor是线程池的真正实现,它的构造方法提供了一系列参数来配置线程池。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)
- corePoolSize:核心线程数,默认情况下会在线程池中一直存活,即使它们处于闲置状态。
- maximumPoolSize:线程池所能容纳的最大线程数,当活动线程数到达此数值后,后续的新任务将被阻塞。
- keepAliveTime:非核心线程闲置时的超时时长,超过次时长后非核心线程将被回收。当ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true时,keepAliveTime同样会作用于核心线程。
- unit:用于指定keepAliveTime参数的时间单位,常用的有TimeUnit.MILLSECONDS、TimeUnit.SECONDS、TimeUnit.MINUTES等。
- workQueue:线程池中的任务队列,通过线程池的execute方法提交的Runnable对象会存储在这个参数中
- threadFactory:线程工厂,为线程池提供创建新线程的功能。ThreadFactory是一个接口,它只有一个方法:Thread newThread(Runnable r){}。
- ThreadPoolExecutor执行任务时遵循的规则:
- 若线程池中的线程数量未达到corePoolSize,那么会直接启动一个核心线程来执行任务。
- 若达到或超过corePoolSize,那么认为会被插入到workQueue排队等待执行。
- 若在步骤2中无法将任务插入,往往是因为workQueue已满,这时若线程数量未达到maximumPoolSize,就会立刻启动一个非核心线程来执行任务。
- 若步骤3中达到了maximumPoolSize,那么就拒绝执行此任务,ThreadPoolExecotor会调用RejectedExecutionHandler的rejectedExecution方法来通知调用者。
三、图片加载中线程池的具体应用
- 对ThreadPoolExecutor进行参数配置(参考AsyncTask中的线程池配置情况)
代码如下:
private static final int CPU_COUNT=Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE=CPU_COUNT+1;
private static final int MAXIMUM_POOL_SIZE=CPU_COUNT*2+1;
private static final long KEEP_ALIVE=10L;
private static final ThreadFactory sThreadFactory=new ThreadFactory() {
private final AtomicInteger mCount=new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r,"ImageLoader#"+mCount.getAndIncrement());//取当前值并自增
}
};
public static final Executor THREAD_POOL_EXECUTOR=newThreadPoolExecutor(
CORE_POOL_SIZE,MAXIMUM_POOL_SIZE,
KEEP_ALIVE,TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(),sThreadFactory);
- 图片的异步加载
- 先从内存缓存中读取图片,若成功则直接返回结果。否则去线程池调用loadBitmap方法(见上篇博文),loadBitmap是同步加载的过程,一般同步加载过程(特别是有网络请求的情况)会比较耗时,所以这里放在在线程池中调用。
- 当图片加载成功后将图片、图片地址和需要绑定的imageView封装成一个LoaderResult对象,通过mMainHandler来访问UI进行UI层面的相关操作。
- 当中的一个细节是:在bindBitmap里对imageView setTag,这样在主线程中给imageView设置图片前先检查uri是否改变,这样可以解决由于View复用导致列表错位的问题。
具体代码如下:
public void bindBitmap(finalString uri,finalImageView imageView,final intreqWidth,final intreqHeight){
imageView.setTag(TAG_KEY_URI,uri);
Bitmap bitmap=loadBitmapFromMemCache(uri);
if(bitmap!=null){
imageView.setImageBitmap(bitmap);
return;
}
final Runnable loadBitmapTask=newRunnable() {
@Override
public void run() {
Bitmap bitmap=loadBitmap(uri,reqWidth,reqHeight);
if(bitmap!=null){
LoaderResult result=new LoaderResult(imageView,uri,bitmap);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT,result).sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
四、后记:异步操作带来的列表错位问题
- 关于列表错位的问题,一般是重用convertView和异步操作同时发生才会出现,这儿要联系getView来理解(之前的博文ListView优化详解有简单介绍)。比如Item8复用Item1的View,如果Item1的数据(比如图片)加载慢,Item8的快,当上滑使Item8可见时,Item8先显示自己的图片,但等到Item1图片也完成加载时Item8的图片变成Item1的图片了。因为它们复用的是同一个View,指向同一块内存。同理若Item1快Item8慢,上滑使Item8可见时先显示Item1的图片。因此,重用convertView和异步操作同时发生时,会出现列表错位的问题。
Demo地址:FindJpg