一般来讲一个网络访问就需要App创建一个线程来执行,但是这也导致了当网络访问比较多的情况下,线程的数目可能积聚增多,虽然Android系统理论上说可以创建无数个线程,但是某一时间段,线程数的急剧增加可能导致系统OOM。在UIL中引入了线程池这种技术来管理线程。合理利用线程池能够带来三个好处。第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

前面我们有讲到ImageLoader.displayImage(…)函数中的图片处理流程,但当时有意忽略了线程方面的额处理。UIL中将线程池相关的东西封装在ImageLoaderEngine类中了。让我们回到图片下载的源代码中,也就是ImageLoader.displayImage(…)函数。

     public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
             ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
         //检查UIL的配置是否被初始化
         checkConfiguration();
         if (imageAware == null) {
             throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
         }
         if (listener == null) {
             listener = emptyListener;
         }
         if (options == null) {
             options = configuration.defaultDisplayImageOptions;
         }

         if (TextUtils.isEmpty(uri)) {
             engine.cancelDisplayTaskFor(imageAware);
             listener.onLoadingStarted(uri, imageAware.getWrappedView());
             if (options.shouldShowImageForEmptyUri()) {
                 imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
             } else {
                 imageAware.setImageDrawable(null);
             }
             listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
             return;
         }
         //计算Bitmap的大小,以便后面解析图片时用
         ImageSize targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
         String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
         engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);

         listener.onLoadingStarted(uri, imageAware.getWrappedView());
         //Bitmap是否缓存在内存?
         Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
         if (bmp != null && !bmp.isRecycled()) {
             L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);

             if (options.shouldPostProcess()) {
                 ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                         options, listener, progressListener, engine.getLockForUri(uri));
                 //处理并显示图片
                 ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
                         defineHandler(options));
                 if (options.isSyncLoading()) {
                     displayTask.run();
                 } else {
                     engine.submit(displayTask);
                 }
             } else {
                 //显示图片
                 options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
                 listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
             }
         } else {
             if (options.shouldShowImageOnLoading()) {
                 imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
             } else if (options.isResetViewBeforeLoading()) {
                 imageAware.setImageDrawable(null);
             }

             ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                     options, listener, progressListener, engine.getLockForUri(uri));
             //启动一个线程,加载并显示图片
             LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
                     defineHandler(options));
             if (options.isSyncLoading()) {
                 displayTask.run();
             } else {
                 engine.submit(displayTask);
             }
         }
     }

注意上面代码块中的第48行和第68行,当需要加载显示图片的时候,相关的task通过engine.submit(...)函数提交执行,那么submit之后发生了什么呢?engine是ImageLoaderEngine类的一个实例,他主要用来响应displayTask的执行。我们跟进ImageLoaderEngine中看看相关的字段和方法。

 class ImageLoaderEngine {

     final ImageLoaderConfiguration configuration;

     private Executor taskExecutor;
     private Executor taskExecutorForCachedImages;
     private Executor taskDistributor;

     private final Map<Integer, String> cacheKeysForImageAwares = Collections
             .synchronizedMap(new HashMap<Integer, String>());
     private final Map<String, ReentrantLock> uriLocks = new WeakHashMap<String, ReentrantLock>();

     private final AtomicBoolean paused = new AtomicBoolean(false);
     private final AtomicBoolean networkDenied = new AtomicBoolean(false);
     private final AtomicBoolean slowNetwork = new AtomicBoolean(false);

     private final Object pauseLock = new Object();

     ImageLoaderEngine(ImageLoaderConfiguration configuration) {
         this.configuration = configuration;

         taskExecutor = configuration.taskExecutor;
         taskExecutorForCachedImages = configuration.taskExecutorForCachedImages;

         taskDistributor = DefaultConfigurationFactory.createTaskDistributor();
     }

     /** Submits task to execution pool */
     void submit(final LoadAndDisplayImageTask task) {
         taskDistributor.execute(new Runnable() {
             @Override
             public void run() {
                 File image = configuration.diskCache.get(task.getLoadingUri());
                 boolean isImageCachedOnDisk = image != null && image.exists();
                 initExecutorsIfNeed();
                 if (isImageCachedOnDisk) {
                     taskExecutorForCachedImages.execute(task);
                 } else {
                     taskExecutor.execute(task);
                 }
             }
         });
     }

     /** Submits task to execution pool */
     void submit(ProcessAndDisplayImageTask task) {
         initExecutorsIfNeed();
         taskExecutorForCachedImages.execute(task);
     }

     private void initExecutorsIfNeed() {
         if (!configuration.customExecutor && ((ExecutorService) taskExecutor).isShutdown()) {
             taskExecutor = createTaskExecutor();
         }
         if (!configuration.customExecutorForCachedImages && ((ExecutorService) taskExecutorForCachedImages)
                 .isShutdown()) {
             taskExecutorForCachedImages = createTaskExecutor();
         }
     }

     private Executor createTaskExecutor() {
         return DefaultConfigurationFactory
                 .createExecutor(configuration.threadPoolSize, configuration.threadPriority,
                 configuration.tasksProcessingType);
     }
     //省略部分代码....
 }

注意到第29行submit(final LoadAndDisplayImageTask task)函数,我们发现这个函数通过taskDistributor.execute来执行一个Runnable对象的run(),从代码中不难知道它就是先试读取磁盘缓存,再根据isImageCachedOnDisk判断文件是否有缓存在磁盘中,最后通过不同的taskExecutor来执行对应的任务。我们注意到这个submit函数中出现了taskExecutorForCachedImages、taskExecutor、taskDistributor这三个对象。通过观察ImageLoaderEngine的类字段(第5~7行),我们发现它们其实都是Executor接口的实例。

我们先来学习一下Executor接口,它规定了线程池的接口。

Executor接口执行已提交的 Runnable 任务的对象。此接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节、调度等)分离开来的方法。通常使用 Executor 而不是显式地创建线程。例如,可能会使用以下方法,而不是为一组任务中的每个任务调用 new Thread(new(RunnableTask())).start():

Executor executor = anExecutor;

executor.execute(new RunnableTask1());

executor.execute(new RunnableTask2());

...

上面这段话,说白了我们就是通过Exccutor对象将线程放入线程池中运行的。

通过观察我们发现,taskExecutorForCachedImages和taskExecutor都是在ImageLoaderEngine.createTaskExecutor()中创建,经过分析我们发现他在DefaultConfigurationFactory.createExecutor中被初始化成ThreadPoolExecutor类型的对象(这是默认情况)。需要注意的是,ThreadPoolExecutor其实是实现了ExecutorService接口的一个实体类。线程池实际表现为 ExecutorService 类的一個实例。通过使用 ExecutorService ,我们可以提交将在未来完成的任务。需要补充说明的是,ExecutorService继承自Executor接口。

接下来让我们看看ThreadPoolExecutor初始化所需要的参数。

创建一个ThreadPoolExecutor需要的参数:

  • corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
  • runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。
    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  • maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
  • ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
  • RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。
    • AbortPolicy:直接抛出异常。
    • CallerRunsPolicy:只用调用者所在线程来运行任务。
    • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    • DiscardPolicy:不处理,丢弃掉。
    • 当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
  • keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
  • TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

接下来,再让我们分析taskDistributor的创建过程。分析发现,taskDistributor在DefaultConfigurationFactory.createTaskDistributor()中被创建,它是一个ThreadPoolExecutor类型的对象,通过Executors线程池工厂创建。官方文档推荐程序员用它来创建线程池,因为它已经配置好常见的线程池情景。接下来让我们来了解一下Executors工厂方法所能创建的线程池类型。

用Executors静态工厂方法创建的线程池类型
    a) newFixedThreadPool:创建一个定长的线程池。达到最大线程数后,线程数不再增长。如果一个线程由于非预期Exception而结束,线程池会补充一个新的线程。
    b) newCachedThreadPool:创建一个可缓存的线程池。当池长度超过处理需求时,可以回收空闲的线程。
    c) newSingleThreadPool:创建一个单线程executor。
    d) newScheduledThreadPool:创建一个定长的线程池,而且支持定时的以及周期性的任务执行。类似于Timer。但是,Timer是基于绝对时间,对系统时钟的改变是敏感的,而ScheduledThreadPoolExecutor只支持相对时间。
         1) Timer是创建唯一的线程来执行所有的timer任务。如果一个任务超时了,会导致其他的TimerTask时间准确性出问题。
         2)如果TimerTask抛出uncheck 异常,Timer将会产生无法预料的行为。因此,ScheduledThreadPoolExecutor可以完全代替Timer。

再回到上文提到的ImageLoaderEngine.submit(...),从函数中分析可以得知:taskDistributor用来尝试读取磁盘中是否有图片缓存,因为涉及磁盘操作,需要用线程来执行。根据是否有对应的图片缓存,将图片加载的任务分发到对应的执行器。如果图片已经缓存在磁盘,则通过taskExecutorForCachedImages执行,如果图片没有缓存在磁盘,则通过taskExecutor执行。我们注意到这三个都实现了Executor接口,那么为什么要将任务细分在三个线程池中进行呢?这其实这跟线程池的调优有关,如果我们将所有的任务都放在同一个线程池中运行当然是可以的,但是这样的话所有的任务就都只能采取同一种任务优先级和运行策略。显然果要有更好的性能,在线程数比较多并且线程承担的任务不同的情况下,App中最好还是按任务的类别来划分线程池。

上面的分析又引出一个问题,我们究竟应该如何配置自己的线程池。

合理的配置线程池

要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

  1. 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
  2. 任务的优先级:高,中和低。
  3. 任务的执行时间:长,中和短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能小的线程,如配置Ncpu+1个线程的线程池。IO密集型任务则由于线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。

建议使用有界队列,有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千。有一次我们组使用的后台任务线程池的队列和线程池全满了,不断的抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞住,任务积压在线程池里。如果当时我们设置成无界队列,线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然我们的系统所有的任务是用的单独的服务器部署的,而我们使用不同规模的线程池跑不同类型的任务,但是出现这样问题时也会影响到其他任务。

接下来,让我们看看UIL中线程池的配置。

让我们来分析一下,taskDistributor由于在每创建一个新的线程的时候都需要读取一下磁盘,属于IO操作。需要图片缓存的应用一般在需要加载图片的时候,同时创建很多(>5)线程,这些线程一般来得猛去的也快,存活时间不必太长。taskDistributor和taskExecutorForCachedImages涉及网络和磁盘的读取和写入操作,比较耗时。主线程数默认为3,感觉定的低了,实际上IO密集的操作应该定得高一点,以便合理利用CPU的。线程优先级(10为最高,1为最低)为4是比较合理的,因为这些操作只需要后台完成即可,优先级太高可能让界面失去响应。

Universal-Image-Loader完全解析--从源代码分析Universal-Image-Loader中的线程池的更多相关文章

  1. 深度分析:Java并发编程之线程池技术,看完面试这个再也不慌了!

    线程池的好处 Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池.在开发过程中,合理地使用线程池,相对于单线程串行处理(Serial Processing ...

  2. 从源代码分析Universal-Image-Loader中的线程池

    一般来讲一个网络访问就需要App创建一个线程来执行,但是这也导致了当网络访问比较多的情况下,线程的数目可能积聚增多,虽然Android系统理论上说可以创建无数个线程,但是某一时间段,线程数的急剧增加可 ...

  3. Android应用Activity、Dialog、PopWindow、Toast窗体加入机制及源代码分析

    [工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处.尊重劳动成果] 1 背景 之所以写这一篇博客的原因是由于之前有写过一篇<Android应用setCont ...

  4. Monkey源代码分析之事件注入

    本系列的上一篇文章<Monkey源代码分析之事件源>中我们描写叙述了monkey是怎么从事件源取得命令.然后将命令转换成事件放到事件队列里面的.可是到如今位置我们还没有了解monkey里面 ...

  5. 线程池ThreadPoolExecutor、Executors参数详解与源代码分析

    欢迎探讨,如有错误敬请指正 如需转载,请注明出处 http://www.cnblogs.com/nullzx/ 1. ThreadPoolExecutor数据成员 Private final Atom ...

  6. Android系统进程间通信Binder机制在应用程序框架层的Java接口源代码分析

    文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6642463 在前面几篇文章中,我们详细介绍了A ...

  7. Openstack本学习笔记——Neutron-server服务加载和启动源代码分析(三)

    本文是在学习Openstack过程中整理和总结.因为时间和个人能力有限.错误之处在所难免,欢迎指正! 在Neutron-server服务载入与启动源代码分析(二)中搞定模块功能的扩展和载入.我们就回到 ...

  8. Glusterfs3.3.1DHT(hash分布)源代码分析

    https://my.oschina.net/uvwxyz/blog/182224 1.DHT简介 GlusterFS使用算法进行数据定位,集群中的任何服务器和客户端只需根据路径和文件名就可以对数据进 ...

  9. Android异步任务处理框架AsyncTask源代码分析

    [转载请注明出处:http://blog.csdn.net/feiduclear_up CSDN 废墟的树] 引言 在平时项目开发中难免会遇到异步耗时的任务(比方最常见的网络请求).遇到这样的问题.我 ...

随机推荐

  1. 用js来实现那些数据结构13(树01-二叉搜索树的实现)

    前一篇文章我们学会了第一个非顺序数据结构hashMap,那么这一篇我们来学学树,包括树的概念和一些相关的术语以及二叉搜索树的实现.唉?为什么不是树的实现,不是二叉树的实现.偏偏是二叉搜索树的实现?嗯, ...

  2. RDO Stack: Failed connect to server

    Issue: When you create an instance, but cannot connect to the VNC Server because of the error messag ...

  3. 剑指Offer——知识点储备-JVM基础

    剑指Offer--知识点储备-JVM基础 1.java内存与内存溢出 1.1 JVM分为哪些区,每一个区干嘛的?(见java虚拟机38页) (1)程序计数器(线程私有) 当前线程执行字节码的信号指示器 ...

  4. 分析RunTime执行命令以及得到返回值

    RunTime执行命令得到返回值 我们有在好好几篇博客里提到过RunTime,比如 JAVA之旅(二十三)--System,RunTime,Date,Calendar,Math的数学运算 Androi ...

  5. PHP + JavaScript + Ajax 实现无刷新页面加载效果

    数据源工厂 Json生成方式1 Json生成方式2 数据搬运工 数据加工师 转换类型 加工展示 结果展示 初始页面 点击按钮之后 总结 今天这个实验的思路就是实现一个无刷新的页面加载效果.具体的思路是 ...

  6. setting.py

    """ Django settings for sitea project. For more information on this file, see https:/ ...

  7. 【安卓开发】Android为什么选择binder

    Binder (Android技术内幕): 在上面这些可供选择的方式中,Android使用得最多也最被认可的还是Binder机制. 为什么会选择Binder来作为进程之间的通信机制呢?因为Binder ...

  8. UNIX网络编程——UNIX域套接字编程和socketpair 函数

    一.UNIX Domain Socket IPC socket API原本是为网络通讯设计的,但后来在socket的框架上发展出一种IPC机制,就是UNIX Domain Socket.虽然网络soc ...

  9. Dynamics CRM 检测访问CRM延迟及带宽的工具

    直接在浏览器中访问如下地址"http://CRMHOST/organization/tools/diagnostics/diag.aspx"(这里的CRMHOST和organiza ...

  10. Android的ProgressBar进度条-android学习之旅(三十一)

    ProgressBar 简介 ProgressBar是一种很常用的Ui,用于给复杂的操作显示进度,提供更好的用户相应.使用setProgress()incrementProgressBy()来设置进度 ...