说明
这篇文章主要是为了演示 OkHttp 和多线程的简单运用,利用多线程和 HTTP 断点续传的特性实现多线程下载。总体分为三种实现发难,并且实现了这三种方案。这里先说明,这只是一个 Demo 程序,为了快速实现功能,所以没有太多设计思想在里面,很多异常情况也没有考虑。所以,不喜勿喷,你如果觉得应该怎样,而不是我这样,那你自己去实现好了。当然,如果对我的方案,或实现上面有更好的想法,欢迎讨论。
多线程下载方案分析
方案一
首先,第一个方案肯定是最简单的,将一个文件的内容按线程数分割任务,每个任务下载一块区域的数据,然后将数据写到各自的临时文件中。每一个任务对应一个线程,所有线程执行完后把临时文件中的内容合并到一个文件中(如下图所示)。
这个方案实现起来很简单,只要懂一点多线程知识都能实现。
方案二
第二种方案骚味高级一些,首先说方案一中的问题。如果不考虑其它因素,单从多线程的角度来看,这种方案不能充分发挥线程的优势。举个栗子,如果有 4 个线程,有 3 个结束了,剩下那个执行很慢,就得等它结束才能执行后续步骤,它手上的任务也不能分出来个其它线程来处理,这样就会拖累整个进度。所以就会出现这样一个场景:忙的很忙,闲的很闲。
所以,为了让每个线程都忙起来,就需要将任务分成诺干个小任务。这样,即使有一个线程慢,那它影响的只是它现在处理的那一小块任务,其它线程执行完当前的任务后,又可以去领取任务了继续来做。
基于上面的分析,可以设定一个阈值(即每个任务下载的文件大小),按这个阈值将整个文件分解成诺干任务,然后放在线程池中,让线程去处理。线程池中的线程数是固定的,一般都是 CPU 的内核数,比如 8 个 /16 个或 32 个。这样就能让每个 CPU 都工作起来(多线程并不是线程越多,执行越快,以后再写文章来说明这个问题)。具体方案如下图所示。
要实现这个方案,就需要用到线程池,要理解线程池的工作原理。如果了解线程池,这个实现起来也不难。
方案三
方案二看上去很完美,但也有两个问题,这里只说一个,另一个留个大家自己去发现,如果你发现了可以在评论区说出来,也可以提出你的解决方案。这里要说的这个问题就是磁盘 I / O 的问题,虽然以现在的硬盘来说基本上不会出现这个问题,but… 请容许我装一波 B 行么?
第三个方案其实和方案二差不多,只是将写文件改成了写缓存,Why?? 因为我牛 B,我乐意!说正经的,如果你的硬盘写入速度很慢,多慢呢?硬盘写入速度 < 你的网速。比如,你是千兆网,下载速度可以破百,而你的硬盘写入速度在一百以内,并且是下载一个很大的文件,只有在这种场景下才可能出现磁盘 I / O 瓶颈(说实话,你的网速都到这个份上了,硬盘应该也不会差到哪里去。所以,实现这个方案,就当学习好了)。
如果出现了磁盘 I / O 的问题,那将会影响每个线程的处理效率。因为每个任务里的下载请求返回数据后,都需要将数据写入一个文件,写入文件慢了就会拖累下载速度。所以,方案三与方案二不同的地方,是将写入临时文件换成写入缓存队列(就是内存),然后另一个线程负责去队列去数据,然后写到文件中。这样即使磁盘慢,那也不会影响下载速度了,如下图所示。
实现了方案二,再实现这个方案就只需要将写入临时文件改成写入缓存队列,合并文件改成一个读缓存队列写文件的线程任务就可以了。这里就需要再了阻塞解队列的概念,同样的,如果你了解什么是阻塞队列,那实现起来也不难了。
要实现以上三种方案,都是有先决条件的:
- 要先得到文件的大小,如果不清楚文件的大小,那如何划分任务?
- 需要目标服务器支持断点续传,如果不支持,也没法将任务分成多个执行,因为每个任务需要下载不同区域的内容,具体到后面方案实现中讲。
以上两点,缺一不可。
未完待续。。。