浅谈Android和java中的多线程下载

为什么要使用多线程下载呢?

究其原因就一个字:"快",使用多线程下载的速度远比单线程的下载速度要快,说到下载速度,决定下载速度的因素一般有两个:

一个是客户端实际的网速,另一个则是服务端的带宽。我们经常使用的是单线程下载,也就是下载一个文件就是开启一个线程去请求下载资源。

这里我们不考虑客户端实际网速因素,因为这个因素多变,不好控制。我们主要考虑的因素就是服务端的带宽。那么服务端是如何给每个客户端分配

它的下载带宽的呢???它分配的原理大致是这样的,服务端只会给请求它的每个线程平均分配相应的下载带宽,这也就说明每个线程享受

服务端CPU的时间片大小是一样的,当其中一个线程的下载时间片完了,尽管此时它的资源没有下载完毕,它也不得不让出CPU资源给下一个线程使用

这实际上也就是操作系统中的时间片轮转算法的原理,所以它的下载带宽分配单位是“线程”,这也就是为什么有时候我们下载资源的过程中速度忽快忽慢的原因。

说到这里,相信你大致明白了为什么多线程要比单线程的下载速度要快了吧。如果不明白,也没有关系,下面我将会通过两张示意图和一个例子来说明

它的原因。

我们假如有下面这样的一个情景:小明、小华、小强,他们都想看同一部电影XXX.avi,所以就同时到同一个服务器上下载XXX.avi.开始的时候三个人都很老实

都只开着一个线程去请求下载资源,这时候服务端带宽假设是30M,那么按照我们上面讲的分配原理,三个人应该平均分的10M的下载速度。

但是小明就实在忍不住,他再也无法忍受这么慢的下载速度,所以他就开始请教高手,经过高手的指点,终于练就多线程的本领,于是乎他就开挂了,

使出了多线程下载的绝招了,顿时其他两个人就懵逼了。他是怎么做的呢?就在其他两个人在下载的时候,他却默默地多开了两个线程,也就是总共开

了三个线程去下载同一部电影,此时服务器就会发现,"咦,这里又多了两个线程,那么也给他们分点吧"。而此时服务器端却不知,其中三个线程是来自

于同一个用户(注意:服务端下载带宽分配单位是"线程"而不是“用户”)。那么服务器就会把原来30M带宽,现在总共有5个线程,再次重新平均分配

最终分得每个线程只有6M,那么小明就很开心,因为他开了3个线程,所以他下载这部电影的速度是18M,而其他两个人则由原来的10M变为现在的6M,

估计其他两个人得砸电脑了,尼玛越下越慢,所以这也就是典型的牺牲别人的速度来提高自己的速度例子。

既然,这个多线程下载这么厉害,那么下面就由小明来讲解其中的奥秘吧。

2、怎么去使用多线程下载??

首先,我们得来说下多线程下载实现的大致思路,以及在使用多线程下载过程应该需要注意的问题。

多线程下载实现的大致思路:

大致思路是这样的,也就是把整个一个文件资源分为若干个部分,然后开启若干个线程,并且使得每个线程负责下载每个子部分的文件,由于

线程下载是异步的,大大缩短了下载的时间,最后将所有的子部分写入到同一个文件中,最后重组得到一个完整的文件。

首先,我们得说下整个资源下载,我们通过网络请求然后可以得到一个文件的整体输入流,然后我们需要得到不是整个文件的输入流,而是

得到每个线程负责下载的子部分文件的输入流。然后得到这些指定大小的输入流,再次写入到我们本地文件中,写入流的时候也需要注意,

每个子部分输入流必须写入相对应的文件位置上,否则会造成后一个写入文件中的输入流会覆盖上一部分写入文件的输入流。

再次整理一下思路我们需要注意哪些问题:

问题一:如何获得整个下载资源的大小

问题二:获得整个文件资源的大小后,如何去拆分这个文件,如何去分配每个子部分文件,并且让不同的子线程来下载,也就是如何

确定每个子线程下载文件的区间(即每个子线程负责下载的子部分文件的开始下载和结束下载位置)

问题三:如何去获得我们需要的指定的大小的输入流,而不是获得由服务端一下把整个文件的输入流

问题四:如何使得每个子线程写入文件合适的起始位置,并且系统默认就是每次往文件中写数据的时候都是从0位置开始写,这样会出现后面

写入的数据,会覆盖前面写入的数据。

3、逐个击破解决上面几个问题,待这些问题都解决了,那么我们的多线程也就实现了

问题一的解决办法:

获取整个下载资源大小,这个很简单,可以直接通过HttpURLConnection网络请求而得到一个HttpURLConnection类型的连接对象

中的getContentLength来得到我们需要下载资源文件的大小。

更重要的是:我们拿到这个文件大小来干什么???其实说白了,就是仿造一个一样大小的空白文件来占用本地存储空间

为什么要这样做呢??其实细心的人就发现,当我们在下载电影或者文件的时候我们会发现在下载目录中会出现一个临时文件

而这个临时文件大小与我们要下载的文件的大小一致,并且这个文件此时是空白的。不信你可以右击查看属性文件大小,

为什么要占用空间,我们可以去设想一下这个情景,假如电脑中的储存空间只剩1G了,而你下载的电影正好1G,电影正在下载过程

假如没有提前占好空间的话,在下载过程中你又下载一个首歌,此时空间明显不足以装下这部电影,那请问这部电影将怎么办?

所以为了防止这种情况出现,也就出现所谓提前占用存储空间。

问题二的解决办法:

如何去拆分整个文件,因为我们要让每个线程去负责每个子部分文件的下载任务,所以直接按照线程的数目来分吧,但是有个问题

就是无法做到每个线程平均分配每个子部分文件的长度,所以我们就采用一个办法,假设有n个线程,就是让前n-1大小一样,最后一个

就包括一些剩余的零头吧,也就是最后一个线程处理最后剩余所有的子部分文件长度。

所以就有如下公式:

前n-1个线程的子部分文件尺寸:  size=len/threadCount

这样也就很容易得到了每个线程负责子部分文件的长度

伪代码:

int size=length/threadCount;

for (int id = 0; id < threadCount; id++) {

//1、确定每个线程的下载区间

//2、开启对应子线程下载

int startIndex=id*size;//确定每个线程的子部分文件的开始位置

int endIndex=(id+1)*size-1;//确定每个线程的子部分文件的结束位置

if (id==threadCount-1) {//判断如果是最后一个线程,直接让它的子部分文件结束位置延伸最后即可,也即文件长度-1

endIndex=length-1;

}

System.out.println("第"+id+"个线程的下载区间为"+startIndex+"--"+endIndex);

问题三的解决办法:

如何去指定确定大小的输入流呢?在HttpURLConnection对象中有个setRequestProperty方法设置头部信息可以拿到拿到指定大小的输入流

conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex);

但是需要注意的一点是:你的请求的服务器必须支持多线程下载,并且才能拿到指定大小输入流

为什么要拿到指定大小的输入流为的就是与划分子部分文件长度对应起来,得到的对应指定大小的输入流通过输出流写入到相应大小的子部分文件中

问题四的解决办法:

防止默认设置(每次都从0位置开始写)的影响使得后面写入的数据会覆盖前面写入的数据,通过RandomAccessFile中的seek方法传入每个子部分文件开始的

位置,也就间接更改了默认每次都从0开始写,从每个子部分文件起始位置写,这样就不会覆盖前面的数据。

RandomAccessFile mAccessFile=new RandomAccessFile(file, "rwd");//"rwd"可读,可写

mAccessFile.seek(startIndex);//表示从不同的位置写文件

通过解决上面四个问题,把整个实现的思路梳理了一下,那么我将实现过程大致总结为以下5点:

1、得到下载资源文件的大小,产生相同大小的随机RandomAccessFile空白文件,来占用空间

2、并且把RandomAccessFile空白文件分割成若干个部分,并且确定每个子部分文件的下载空间

3、开启对应的子线程

4、从网络服务器拿到指定大小的部分输入流

5、从RandomAccessFile文件的不同的开始位置开始往其中写入我们得到对应的指定大小的输入流

Main

package com.mikyou.multithread;

import java.io.File;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

/**
 * @author zhongqihong
 * 多线程下载
 * */
public class Main {
	public static final String PATH="http://120.203.56.190:8088/upload/mobilelist.xml";
	public static int threadCount=3;//进行下载的线程数量

	public static void main(String[] args) {
		try {
			URL url=new URL(PATH);
			HttpURLConnection conn=(HttpURLConnection) url.openConnection();
			conn.setRequestMethod("GET");
			conn.setConnectTimeout(8000);
			conn.setReadTimeout(8000);
			conn.connect();
			if (conn.getResponseCode()==200) {
				int length=conn.getContentLength();//返回文件大小
				//占据文件空间
				File file =new File("mobilelist.xml");
				RandomAccessFile mAccessFile=new RandomAccessFile(file, "rwd");//"rwd"可读,可写
				mAccessFile.setLength(length);//占据文件的空间
				int size=length/threadCount;
				for (int id = 0; id < threadCount; id++) {
					//1、确定每个线程的下载区间
					//2、开启对应子线程下载
					int startIndex=id*size;
					int endIndex=(id+1)*size-1;
					if (id==threadCount-1) {
						endIndex=length-1;
					}
					System.out.println("第"+id+"个线程的下载区间为"+startIndex+"--"+endIndex);
					new DownLoadThread(startIndex, endIndex, PATH, id).start();
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

DownLoadThread

package com.mikyou.multithread;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;

public class DownLoadThread extends Thread{
	private int startIndex,endIndex,threadId;
	private String urlString;
	public DownLoadThread(int startIndex,int endIndex,String urlString,int threadId) {
		this.endIndex=endIndex;
		this.startIndex=startIndex;
		this.urlString=urlString;
		this.threadId=threadId;
	}
	@Override
	public void run() {
		try {

			URL url=new URL(urlString);
			HttpURLConnection conn=(HttpURLConnection) url.openConnection();
			conn.setRequestMethod("GET");
			conn.setConnectTimeout(8000);
			conn.setReadTimeout(8000);
			conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex);//设置头信息属性,拿到指定大小的输入流
			if (conn.getResponseCode()==206) {//拿到指定大小字节流,由于拿到的使部分的指定大小的流,所以请求的code为206
				InputStream is=conn.getInputStream();
				File file =new File("mobilelist.xml");
				RandomAccessFile mAccessFile=new RandomAccessFile(file, "rwd");//"rwd"可读,可写
				mAccessFile.seek(startIndex);//表示从不同的位置写文件
				byte[] bs=new byte[1024];
				int len=0;
				int current=0;
				while ((len=is.read(bs))!=-1) {
					mAccessFile.write(bs,0,len);
					current+=len;
					System.out.println("第"+threadId+"个线程下载了"+current);

				}
				mAccessFile.close();
				System.out.println("第"+threadId+"个线程下载完毕");

			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		super.run();
	}
}

运行结果:

时间: 2024-10-09 04:00:51

浅谈Android和java中的多线程下载的相关文章

浅谈Android系统开发中LOG的使用

文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6581828 在程序开发过程中,LOG是广泛使用的用来记录程序执行过程的机制,它既可以用于程序调试,也可以用于产品运营中的事件记录.在Android系统中,提供了简单.便利的LOG机制,开发人员可以方便地使用.在这一篇文章中,我们简单介绍在Android内核空间和用户空间中LOG的使用和查看方法. 一. 内核开发时LOG的使用.Android内核

浅谈javascript和java中的数组

javascript中的数组 数组的创建 直接创建方式  var str = ['java', 'js']; 使用new创建方式: var a = new Array(10);  //  定义长度为10的数组(可变) 另类new创建方式:var a = new Array(1, 2, 3, 4, 5);  var b = [1, 2, 3, 4, 5]; 二维数组(多维)创建方式:var a = new Array([1,2,3], [4,5,6], [7,8,9]);  var b = [[1

源码浅谈(一):java中的 toString()方法

前言: toString()方法 相信大家都用到过,一般用于以字符串的形式返回对象的相关数据. 最近项目中需要对一个ArrayList<ArrayList<Integer>> datas  形式的集合处理. 处理要求把集合数据转换成字符串形式,格式为 :子集合1数据+"#"+子集合2数据+"#"+....+子集合n数据. 举例: 集合数据 :[[1,2,3],[2,3,5]]  要求转成为 "[1,2,3]#[2,3,5]"

浅谈javascript和java中的字符串

javascript字符串操作 一.字符串的创建 创建一个字符串有几种方法. 1.最简单的是用引号将一组字符包含起来  var myStr = "Hello, String!";// 在js中单双引号没有区别 2.可使用如下语句:var myStr1 = new String("Hello, String!"); 1 2 console.log(typeof myStr);//"string" console.log(typeof myStr1)

浅谈Android保护技术__代码混淆

浅谈Android保护技术__代码混淆 代码混淆 代码混淆(Obfuscated code)亦称花指令,是将计算机程序的代码,转换成一种功能上等价,但是难于阅读和理解的形式的行为.将代码中的各种元素,如变量,函数,类的名字改写成无意义的名字.比如改写成单个字母,或是简短的无意义字母组合,甚至改写成"__"这样的符号,使得阅读的人无法根据名字猜测其用途.对于支持反射的语言,代码混淆有可能与反射发生冲突.代码混淆并不能真正阻止反向工程,只能增大其难度.因此,对于对安全性要求很高的场合,仅仅

浅谈Android应用保护(二):Anti-Analysis的方法和工具

本文内容翻译自国外文献,原文链接请参看文章底部 之前讲到过,应用开发者为了保护自己的应用不被别人分析和篡改,会将应用的安全性寄托在某个(些)机制上.可以被用来保护应用的机制有很多,效果和实现难度也是各有特点.有这样一类应用保护方法,叫做针对逆向工具的对抗(Anti-Analysis). 针对逆向工具的对抗,简单来讲就是利用逆向工具自身的缺陷或者是Android特有的机制,使应用逆向分析工具在工作过程中失效或者报错崩溃,分析过程无法继续实施.从而达到保护应用的目的. 这种保护应用的方式的优点就是实

浅谈Android多屏幕的事

浅谈Android多屏幕的事 一部手机可以同时看片.聊天,还可以腾出一支手来撸!这么吊的功能(非N版本,非第三方也能实现,你不知道吧)摆在你面前,你不享用?不关注它是怎样实现的?你来,我就满足你的欲望! 一部手机可以同时看片.聊天,还可以腾出一支手来撸==!就像这样: 是时候告别来回切换应用屏幕的酸爽了,还可以在分屏模式下两Activity间直接拖放数据! 好高大上的样子!这是怎么实现的?别急,我们一一道来: kitkat(4.4)版本对多任务分屏的实现 由于相关的代码和功能被封装及隐藏起来,所

浅谈 Android Service

 浅谈Android Service的基本用法: 关于Service最基本的用法自然是启动和停止操作. 启动Service有两种方式: 1.通过startService(Intent intent)方式启动,启动时会自动执行onCreate(),onStartCommand()方法. 2.通过bindService(Intent intent,ServiceConnection connection,int flag) 第一个参数是一个Intent对象,第二个参数是连接Service的实例,

浅谈Android五大布局

Android的界面是有布局和组件协同完成的,布局好比是建筑里的框架,而组件则相当于建筑里的砖瓦.组件按照布局的要求依次排列,就组成了用户所看见的界面.Android的五大布局分别是LinearLayout(线性布局).FrameLayout(单帧布局).RelativeLayout(相对布局).AbsoluteLayout(绝对布局)和TableLayout(表格布局). LinearLayout: LinearLayout按照垂直或者水平的顺序依次排列子元素,每一个子元素都位于前一个元素之后