四:缓存管理
在Thread切分文件时,会创建非常多的DownloadBlock,为了减少创建、销毁Block所带来对效率上的影响,我们会把已经创建好的DownloadBlock放入到缓存中,当Download需要DownloadBlock时直接从缓存中取得。即我们使用缓存来对已经申请的内存重复利用。
在每个Download类中都有一个缓存池,即ConcurrentLinkedQueue<DownloadBlock> blockCache;
而整个的系统Downloader也会有一个大的ConcurrentLinkedQueue<DownloadBlock> blockCache;
那么这两者是怎样的关系呢?
在Download类对文件进行切分的时候,需要创建DownloadBlock,而DownloadBlock是根据getDownloadBlock函数来获取到的。
当当前的DownloadBlock使用DownloadThread下载完毕之后,他所拥有的这个DownloadBlock会交给其所在的Download,以供剩余新建的DownloadBlock使用。
在每个Download类中都有一个缓存池,即ConcurrentLinkedQueue<DownloadBlock> blockCache;
而整个的系统Downloader也会有一个大的ConcurrentLinkedQueue<DownloadBlock> blockCache;
那么这两者是怎样的关系呢?
在Download类对文件进行切分的时候,需要创建DownloadBlock,而DownloadBlock是根据getDownloadBlock函数来获取到的。
/** * 从block cache获取一块DownloadBlock。 * cache中的block都是已经下载完毕的。 * * @param start block块的下载开始位置 * @param length block块需要下载的长度 * @return 返回一个创建好的block块。 */ public DownloadBlock getDownloadBlock(long start, long length){ DownloadBlock block; //先从当前的Download中看是否有 if(blockCache.size() > 0) block = blockCache.poll(); else{ block = Downloader.getInstance().getDownloadBlock(); block.setDownload(this); } block.setStart(start); block.setLength(length); return block; }上面的代码中,首先会从当前Download所拥有的Cache中获取DownloadBlock,如果有空闲的DownloadBlock,则直接取出。如果没有则需要从当前系统中去申请。
当当前的DownloadBlock使用DownloadThread下载完毕之后,他所拥有的这个DownloadBlock会交给其所在的Download,以供剩余新建的DownloadBlock使用。
block.getDownload().putDownloadBlock(block);
而当整个文件都下载完毕之后,需要把当前Download的所有缓存归还给系统(Downloader)
for(DownloadBlock block : blockCache){ Downloader.getInstance().putDownloadBlock(block); }
总结:上面的代码演示了一个简单的内管管理模块。而为了频繁的申请内存所带来的效率损耗,我们通过建立blockCache用于缓存当前已经申请好的DownloadBlock, 这样可以对已经申请的内存重复利用。
五. 高效的任务管理分析与方案
在浏览网页内容时,网页内容一般是超出屏幕高度的,需要向下滚屏的。如下图所示,用户浏览该网页,迅速滚屏到最下。于是图片的载入是顺序的,用户停留的网页区域图片最后才载入。
我们希望的体验是,如下图所示,用户在浏览网页,已有图片1和图片2已经或正在下载,然后迅速滚屏到最下的过程中,下载任务的优先级不断调整,最终如图所示数字,用户所看到的网页区域最高优先级下载。
我们先实验一下,在限速环境下下载10个相同文件,下面是分别在拥挤与排队模式下的时间轴:
可以看到,一拥而上的下载过程,每个任务的速度被平摊了,所有任务只能全部最后时段完成,而排队模式部分任务排队,部分先快速完成,排队任务随后也能快速完成。
争抢能力与网络连接数
一般来讲网络连接数越多,整体争抢到的资源越多,但不是绝对的线性正比关系的,而且增多到一定程度后网络质量下降。这些取决于复杂的网络环境和本机其他程序的影响。
所以,通过测试我们会通过在不同网络环境下测试,得到最大连接数的参考值。而且,我们无法回避的是,在有些情况下有可能出现,由于最大连接数的限制,而性能没有不限制的好。这里如何动态调整最大连接数,通过技术手段探知网络状况,从而智能调整,应该是一个很好的研究方向。
前后台任务
前台任务,用户操作而等待内容。这是最高优先级的,而且随着用户新操作的出现,新任务赋予更高优先级。可以理解为插到队首。
后台任务,不紧急的或预备给用户使用的。
由于上面的分析,得到通用的方案是:多个优先级的队列。对优先级编号,每个优先级的队列都可以通过插队首或加队尾的方式添加任务。另外还可以提供调整优先级,移动任务的功能。
争抢能力与网络连接数
一般来讲网络连接数越多,整体争抢到的资源越多,但不是绝对的线性正比关系的,而且增多到一定程度后网络质量下降。这些取决于复杂的网络环境和本机其他程序的影响。
所以,通过测试我们会通过在不同网络环境下测试,得到最大连接数的参考值。而且,我们无法回避的是,在有些情况下有可能出现,由于最大连接数的限制,而性能没有不限制的好。这里如何动态调整最大连接数,通过技术手段探知网络状况,从而智能调整,应该是一个很好的研究方向。
前后台任务
前台任务,用户操作而等待内容。这是最高优先级的,而且随着用户新操作的出现,新任务赋予更高优先级。可以理解为插到队首。
后台任务,不紧急的或预备给用户使用的。
由于上面的分析,得到通用的方案是:多个优先级的队列。对优先级编号,每个优先级的队列都可以通过插队首或加队尾的方式添加任务。另外还可以提供调整优先级,移动任务的功能。
六. 单例模式
Downloader 代表整个下载系统,整个系统中只有一个实例对象,因此我们需要保证系统中只有一个实例对象。 OK ,我们看如何保证系统中只有一个单例模式:
为了保证系统中只有一个实例,就需要限制构造函数的使用,因为如何用户可以随意调用构造函数,其就可以任意的建立多个实例。因此我们将构造函数定义为private
private Downloader(DownloaderConfig config) { super(); this.config = config; start(); }而为了能够得到一个实例,我们在该类中定义了一个静态变量
private static Downloader downloader = null;
然后再通过一个public函数得到该Downloader的实例
public static Downloader getInstance(){ if(downloader == null) downloader = new Downloader(new DownloaderConfig()); return downloader; }
函数之所以定义为static,是因为我们无法通过建立类对象来调用类的方法,只能通过类名称来调用该方法,因此必须设定为static类型。
这样通过这种方式,我们就可以保证系统中只实例化一个 Downloader对象
总结
这个系列总共3篇文章,我们通过这3篇文章讲解了基于Java多线程实现一个下载器的细节,包括如何利用多线程对单个文件进行下载,并实时计算下载速率。以及如何对多个下载进行管理、调度以及内存的管理。
Further Reading and Reference:
- 《Java Concurrency in Practice》 Java并发编程一本非常经典的书籍,每看一次都有一些新的体会,写的确实非常棒。
- 《Effective Java》 这本书讲解了Java实践中一些值得注意的关键问题。读了之后收获颇丰。
- http://www.blogjava.net/Rexcj/archive/2008/07/27/217793.html 这篇文章演示了如何使用JAVA实现HTTP多线程下载,其还实现了断点恢复功能以及下载速率统计统计等功能。
- http://www.blogjava.net/xylz/archive/2010/07/08/325587.html 《深入浅出 Java Concurrency》,这个系列通过源码剖析讲解了java.util.concurrent的一些特性,内容质量高,值得一看。
-
http://djt.qq.com/thread-28516-1-3.html腾讯大讲堂上关于Q+的HTTP任务管理