多线程下载总结
线程池的概念
Java 5之后引入了Executor框架,使用了线程池机制,位于java.util.cocurrent包中。使用Executor框架可以更加方便简单地控制线程启动,执行和关闭,并且更加节约内存开销。
首先介绍一下Executor框架的几个重要类:
- Executor
Executor接口中只定义了一个方法execute(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类。 ExecutorService
ExecutorService接口继承自Executor接口,它提供了更丰富的实现多线程的方法,比如,ExecutorService提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorServiceExecutors
Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口:public static ExecutorService newFixedThreadPool(int nThreads)
创建固定数目线程的线程池。public static ExecutorService newCachedThreadPool()
创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。public static ExecutorService newSingleThreadExecutor()
创建一个单线程化的Executor。public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。自定义线程池
通过ThreadPoolExecutor类,它有多个构造方法来创建线程池,用该类很容易实现自定义的线程池public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue), RejectedExecutionHandler handler
根据实际情况合理地配置构造方法的参数才能获得效率最高,性能最好的线程池,几个重要参数如下:
corePoolSize:线程池中所保存的核心线程数,包括空闲线程。
maximumPoolSize:池中允许的最大线程数。
keepAliveTime:线程池中的空闲线程所能持续的最长时间,当线程池中的线程数量大于corePoolSize时,这些多出的线程如果空闲时间超过持续时间keepAliveTime,则将会被销毁。
unit:持续时间的单位,其参数为java.util.concurrent.TimeUnit中的几个静态属性,TimeUnit.SECONDS,TimeUnit.MINUTES等。
workQueue:任务执行前保存任务的队列,仅保存由execute方法提交的Runnable任务。常见的几种队列类型如下:
- SynchronousQueue 直接提交策略, 它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。
- LinkedBlockingQueue 无界队列,若其构造函数带一个规定大小的参数,生成的BlockingQueue有大小限制,若不带大小参数,所生成的BlockingQueue的大小由Integer.MAX_VALUE来决定.其所含的对象是以FIFO(先入先出)顺序排序的。使用无界队列(不具有预定义容量的LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize,因此,maximumPoolSize 的值也就无效了。当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列
- ArrayBlockingQueue 有界队列,有界队列有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
PriorityBlockingQueue 优先级队列,优先级队列类似于LinkedBlockQueue,同样可以在其构造函数中规定队列大小,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数的Comparator决定的顺序。
handler: 线程池对拒绝任务的处理策略,常见的策略包括四种:
- 在默认的 ThreadPoolExecutor.AbortPolicy 中,处理程序遭到拒绝将抛出运行时 RejectedExecutionException
- 在 ThreadPoolExecutor.CallerRunsPolicy 中,线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
- 在 ThreadPoolExecutor.DiscardPolicy 中,不能执行的任务将被删除
在 ThreadPoolExecutor.DiscardOldestPolicy 中,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)
一个任务通过execute(Runnable)添加到线程池之后,线程池工作流程如下:
- 当线程池中的线程数量小于corePoolSize,即使此时线程池中的线程都处于空闲状态,也会创建新的线程来处理添加的任务。
- 当线程池中的线程数量大于等于corePoolSize,但是缓冲队列workQueue未满,那么任务将被放入缓冲队列中。
- 如果线程池中的线程数量大于corePoolSize,且缓冲队列已满,但是线程池中的线程数量小于maximumPoolSize,则此时会立即创建新的线程来处理被添加的任务。
- 如果线程池中的线程数量大于corePoolSize,缓冲队列已满,并且线程池中的线程数量等于maximumPoolSize,此时则通过handler所指定的策略来处理此任务。
多线程下载的实现
此多线程下载方案支持多任务,多线程同时下载。下载方式使用HttpURLConnection,线程池使用了PriorityBlockingQueue,支持通过设置优先级安排任务执行顺序,线程池corePoolSize默认为availableProcessors * 2,即可用处理器个数的两倍,maximumPoolSize默认为128,缓冲队列PriorityObjectBlockingQueue默认容量为10,也可根据实际情况自定义设置。大体流程如下:
- 用户调用下载接口,传入下载配置参数
- 简单条件过滤(关键是否为空判断)后,立即创建一线程并放入线程池中执行
- 该线程进行参数初始化之后,获取服务端文件信息,根据文件大小和下载线程数分配每个线程的下载量,并保存到数据库
- 开始下载,创建N个线程放入线程池中进行下载,实时返回下载进度,每条线程下载完成后判断是否所有线程都下载完成
- 返回下载结果,若都完成则返回成功,若有一个线程下载失败,则返回下载失败,停止其他线程的下载。
此下载方案参考xUtils的线程池模型,结合对单任务使用多线程进行下载,若有不合理之处,请及时指出。