为什么要使用多线程下载呢?
究其原因就一个字:"快",使用多线程下载的速度远比单线程的下载速度要快,说到下载速度,决定下载速度的因素一般有两个:
一个是客户端实际的网速,另一个则是服务端的带宽。我们经常使用的是单线程下载,也就是下载一个文件就是开启一个线程去请求下载资源。
这里我们不考虑客户端实际网速因素,因为这个因素多变,不好控制。我们主要考虑的因素就是服务端的带宽。那么服务端是如何给每个客户端分配
它的下载带宽的呢???它分配的原理大致是这样的,服务端只会给请求它的每个线程平均分配相应的下载带宽,这也就说明每个线程享受
服务端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();
}
}
运行结果: