主程序

/**
 * 客户端
 */
package ThreadDownload;

public class Client {
	public String urlFile; // 网络文件地址
	public int threadNum; // 需要启动下载的线程数
	public String localFilePath; // 需要保存下载文件的本地地址,保证该目录下没有名为"tmp"的文件夹

	/**
	 * 客户端自己设定
	 */
	public Client() {
		urlFile = "http://dl.google.com/android/android-sdk_r12-windows.zip";
		threadNum = 9;
		localFilePath = "d://";
	}

	private void start() {
		Thread thread = new Thread(new MultiThreadGetFile(urlFile,
				localFilePath, threadNum));
		thread.start();
	}

	public static void main(String[] args) {
		Client client = new Client();
		client.start();
	}
}


多线程下载调度程序

/**
 * 多线程下载调度程序
 */
package ThreadDownload;

import java.io.File;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.StringTokenizer;

public class MultiThreadGetFile extends Thread {
	public long startPos = 0;
	public long endPos = 0;
	public String currentFileThreadName; // 要带上完整的路径
	public String urlFile; // 网络文件地址
	public String urlFileName; // 网络文件名
	public String localFilePath; // 需要保存下载文件的本地地址
	public int threadNum; // 需要启动下载的线程数
	public long[] eachThreadLength; // 每个线程要下载的文件分块的大小
	public long urlFileLength; // 网络文件的大小
	URL url;
	HttpURLConnection httpURLConnection;
	public static boolean[] checkList; // 检测线程

	/**
	 * @param urlFile 网络文件地址
	 * @param localFilePath 需要保存下载文件的本地地址
	 * @param threadNum 需要启动下载的线程数
	 */
	public MultiThreadGetFile(String urlFile, String localFilePath,
			int threadNum) {
		super();
		this.urlFile = urlFile;
		this.localFilePath = localFilePath;
		this.threadNum = threadNum;
	}

	/**
	 * 确定每个线程文件最终要写的文件的大小
	 */
	private void init_getEachThreadLength() {
		long l;
		l = urlFileLength / threadNum;
		for (int i = 0; i < threadNum; i++) {
			if (i == threadNum - 1) {
				eachThreadLength[i] = urlFileLength - i * l;
			} else {
				eachThreadLength[i] = l;
			}
		}
	}

	/**
	 * 得到传递过来的带路径的文件的文件名 eg:GetFileName("d://test/test.txt") 返回为:test.txt
	 */
	private String getFileName(String file) {
		StringTokenizer st = new StringTokenizer(file, "/");
		while (st.hasMoreTokens()) {
			file = st.nextToken();
		}
		return file;
	}

	private void init() {
		// 创建一个临时文件夹tmp
		if (!new File(localFilePath + "tmp").mkdir()) {
			// System.out.println("创建文件夹失败!");
		}
		eachThreadLength = new long[threadNum];
		try {
			url = new URL(urlFile);
			// 此处的Connection仅仅是用于获取服务端的要下载的资源的名称和资源的大小,所以一旦得到后就关闭
			httpURLConnection = (HttpURLConnection) url.openConnection();
			urlFileLength = Long.parseLong(httpURLConnection
					.getHeaderField("Content-Length"));
			urlFileName = url.getFile(); // 取得在服务器上的路径及文件名
			urlFileName = getFileName(urlFileName); // 只取得文件名
			init_getEachThreadLength(); // 确定每个线程最终要写的文件的大小
			httpURLConnection.disconnect();
			checkList = new boolean[threadNum + 1]; // 记载每个线程是否下载完毕

			for (int i = 1; i <= threadNum; i++) {
				if (i > 1) {
					startPos = startPos + eachThreadLength[i - 2];
				}
				endPos = startPos + eachThreadLength[i - 1];
				currentFileThreadName = localFilePath + "tmp\\" + urlFileName
						+ ".part" + i;
				System.out.println("part" + i + ": " + startPos + "--->"
						+ endPos + ", size: " + (endPos - startPos));
				Thread thread = new Thread(new GetFileThread(startPos, endPos,
						currentFileThreadName, urlFile, i));
				thread.start();
				checkList[i] = false; // 表示该线程开始

			}
			Thread monitorThread = new Thread(new MonitorThread(threadNum,
					localFilePath, localFilePath + "tmp"));
			monitorThread.start();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void run() {
		init();
	}
}

下载线程

/**
  * 下载线程
  * 原理:
  * 根据传入的下载开始点以及文件下载结束点,利用HttpURLConnection的RANGE属性
  * 从网络文件开始下载,并结合判断是否下载的文件大小已经等于(文件下载结束点-下载开始点)
  * 这里结合断点续传原理,可以更快、更有效的下载文件
  */
package ThreadDownload;

import java.io.BufferedInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;

public class GetFileThread extends Thread {
	public long startPos; // 传入的文件下载开始点
	public long endPos; // 传入的文件下载结束点
	public String currentFileThreadName; // 当前线程的完程路径及名字
	public String urlFile; // 网络文件地址
	int currentThread; // 当前线程,用于下载完成后将对应的检测标志设为true,表示下载完成

	/**
	 * @param startPos 传入的文件下载开始点
	 * @param endPos 传入的文件下载结束点
	 * @param currentFileThreadName 当前线程的完程路径及名字
	 * @param urlFile 网络文件地址
	 * @param currentThread 当前线程
	 */
	public GetFileThread(long startPos, long endPos,
			String currentFileThreadName, String urlFile, int currentThread) {
		super();
		this.startPos = startPos;
		this.endPos = endPos;
		this.currentFileThreadName = currentFileThreadName;
		this.urlFile = urlFile;
		this.currentThread = currentThread;
	}

	private boolean fileExist(String pathAndFile) {
		File file = new File(pathAndFile);
		if (file.exists())
			return true;
		else
			return false;
	}

	private long fileSize(String pathAndFile) {
		long fileSize = 0;
		File filet = new File(pathAndFile);
		fileSize = filet.length();
		return fileSize;
	}

	private void fileRename(String fName, String nName) {
		File file = new File(fName);
		file.renameTo(new File(nName));
		file.delete();
	}

	public void run() {
		URL url = null;
		HttpURLConnection httpURLConnection = null;

		BufferedInputStream bis = null; // 缓存流
		int len = 0;
		byte[] bt = new byte[1024]; // 缓冲区
		DataOutputStream dos = null;
		FileOutputStream fos = null;
		RandomAccessFile raf = null;

		String localFile = currentFileThreadName; // 文件保存的地方及文件名,具体情况可以改
		String localFile_tp = localFile + ".tp"; // 未下载完文件加.tp扩展名,以便于区别
		long fileSize = 0; // 在断点续传中,用于取得当前文件已经下载的大小
		long totalSize = 0; // 当前块要下载的文件总大小

		try {
			url = new URL(urlFile);
			httpURLConnection = (HttpURLConnection) url.openConnection();
			totalSize = endPos - startPos; // 取得该快实际要写的文件大小
			long downSize = 0; // 已经下载的大小

			// 确定临时文件是否存在
			if (fileExist(localFile_tp)) { // 采用断点续传,这里的依据是看下载文件是否在本地有.tp有扩展名同名文件
				System.out.println("线程" + currentThread + "文件正在续传...");
				fileSize = new File(localFile_tp).length(); // 取得已经下载的大小,以便确定随机写入的位置
				downSize = fileSize; // 下载大小
				fileSize = startPos + fileSize; // 取得文件开始写入点
				// httpURLConnection属性的设置一定要在得到输入流之前,否则会报已经连接的错误
				httpURLConnection.setRequestProperty("RANGE", "bytes="
						+ fileSize + "-");
				// 设置接受信息
				httpURLConnection.setRequestProperty("Accept",
						"image/gif,image/x-xbitmap,application/msword,*/*");
				raf = new RandomAccessFile(localFile_tp, "rw"); // 随机方式读取
				raf.seek(downSize); // 定位指针到downSize已下载文件大小的位置
				bis = new BufferedInputStream(
						httpURLConnection.getInputStream()); // 缓存流

				while ((len = bis.read(bt)) > 0) {
					if (downSize < (endPos - startPos)) {
						downSize = downSize + len;
						// 说明上一步是多加了一个downSize,这里再减去一个downSize
						if (downSize > (endPos - startPos)) {
							len = (int) ((endPos - startPos) - (downSize - len));
						}
						raf.write(bt, 0, len);
					} else {
						break;
					}
				}
				System.out.println("线程" + currentThread + "文件续传完成...");
			}
			// 采用原始下载,但保证该文件没有下载
			else if (!fileExist(localFile)) {
				// 设置断点续传的开始位置
				httpURLConnection.setRequestProperty("RANGE", "bytes="
						+ startPos + "-");
				bis = new BufferedInputStream(
						httpURLConnection.getInputStream());
				fos = new FileOutputStream(localFile_tp); // 没有下载完毕就将文件的扩展名命名.tp
				dos = new DataOutputStream(fos);
				System.out.println("线程" + currentThread + "正在接收文件...");
				while ((len = bis.read(bt)) > 0) {
					// 确定没有下载完毕
					if (downSize < (endPos - startPos)) {
						downSize = downSize + len;
						// 如果当前下载的加上要下载的已经超过要求的下载范围
						if (downSize > (endPos - startPos)) {
							// 就只取满足要求的下载部分
							len = (int) ((endPos - startPos) - (downSize - len));
						}
						dos.write(bt, 0, len); // 写文件
					} else {
						break;
					}
				}
			}

			// 下载完毕后,将文件重命名
			if (fileSize(localFile_tp) == totalSize) {
				fileRename(localFile_tp, localFile);
			}
			MultiThreadGetFile.checkList[currentThread] = true;
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				if (bis != null) {
					bis.close();
				}
				if (dos != null) {
					dos.close();
				}
				if (fos != null) {
					fos.close();
				}
				if (raf != null) {
					raf.close();
				}
			} catch (IOException ioe) {
				ioe.printStackTrace();
			}
		}
	}
}



监视线程

/**
 * 监视线程,检测其它的线程是否已经运行完毕
 * 原理:
 * 在MultiThreadGetFile里定义一个全局静态boolean数组,在启动每个GetFileThread的时候
 * 就将对应的数组的值设为false,当对应线程完成后就把对应的数组设为true
 * 在当前线程采用不停检测是否所有数组的值都为true,
 * 如是那就说明所有的线程已经运行完毕,如果没有就继续检测。
 * 等到所有的GetFileThread线程都完成后,那么就调用文件拼合线程,合并下载的文件块并删除 临时文件块。
 */
package ThreadDownload;

public class MonitorThread extends Thread {
	public int totalThread;
	public String localFilePath;
	public String localFilePath_tmp;

	/**
	 * @param totalThread 总线程数
	 * @param localFilePath 本地下载文件存放路径
	 * @param localFilePath_tmp 正在下载的文件存放路径
	 */
	public MonitorThread(int totalThread, String localFilePath,
			String localFilePath_tmp) {
		super();
		this.totalThread = totalThread;
		this.localFilePath = localFilePath;
		this.localFilePath_tmp = localFilePath_tmp;
	}

	public void run() {
		boolean isRun = true;
		int allStop = 0;
		while (isRun) {
			allStop = 0;
			for (int i = 1; i <= totalThread; i++) {
				if (MultiThreadGetFile.checkList[i] == true) {
					allStop++;
				}
			}
			try {
				Thread.sleep(500);
			} catch (Exception e) {
				e.printStackTrace();
			}
			// 说明都已经下载完毕,此时停止线程下载
			if (allStop == totalThread) {
				isRun = false;
			}
		}
		// 到此,说明九个线程均已下载完毕,开始合并文件的线程操作
		Thread thread = new Thread(new FileCombination(localFilePath,
				localFilePath_tmp));
		thread.start();
	}

}



文件合并线程

/**
 * 合并文件:合并由拆分文件拆分的文件
 * 要求将拆分文件放到一个文件中
 * 主要利用随机文件读取和文件输入输出流
 */
package ThreadDownload;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;
import java.util.StringTokenizer;

public class FileCombination extends Thread {
	public String srcDirectory = null; // 拆分文件存放的目录
	public String saveDirectory; // 结果文件存放目录
	public String[] separatedFiles; // 存放所有拆分文件名
	public String fileRealName = ""; // 据拆分文件名确定现在原文件名
	public String[][] separatedFilesAndSize; // 存放所有拆分文件名及分件大小
	public int FileNum = 0; // 确定文件个数

	/**
	 * @param srcDirectory 拆分文件存放的目录
	 * @param saveDirectory 结果文件存放目录
	 */
	public FileCombination(String trueDirectory, String srcDirectory) {
		super();
		this.srcDirectory = srcDirectory;
		this.saveDirectory = trueDirectory;
	}

	/**
	 * @param sFileName 任意一个拆分文件名
	 * @return 原文件名
	 */
	private String getRealName(String sFileName) {
		StringTokenizer st = new StringTokenizer(sFileName, ".");
		return st.nextToken() + "." + st.nextToken();
	}

	/**
	 * @param FileName 拆分的文件名
	 * @return 取得指定拆分文件模块的文件大小
	 */
	private long getFileSize(String FileName) {
		FileName = srcDirectory + "//" + FileName;
		return (new File(FileName).length());
	}

	/**
	 * 生成一些属性,做初使化
	 * 
	 * @param drictory 拆分文件属性
	 */
	private void getFileAttribute(String drictory) {
		File file = new File(drictory);
		separatedFiles = new String[file.list().length];// 依文件数目动态生成一维数组,只有文件名
		separatedFiles = file.list();
		// 依文件数目动态生成二维数组,包括文件名和文件大小
		// 第一维装文件名,第二维为该文件的字节大小
		separatedFilesAndSize = new String[separatedFiles.length][2];
		Arrays.sort(separatedFiles); // 排序
		FileNum = separatedFiles.length; // 当前文件夹下面有多少个文件
		for (int i = 0; i < FileNum; i++) {
			separatedFilesAndSize[i][0] = separatedFiles[i]; // 文件名
			separatedFilesAndSize[i][1] = String
					.valueOf(getFileSize(separatedFiles[i])); // 文件大小
		}
		fileRealName = getRealName(separatedFiles[FileNum - 1]); // 取得文件分隔前的原文件名
	}

	/**
	 * 合并文件:利用随机文件读写
	 * 
	 * @return true 成功合并文件
	 */
	private boolean CombFile() {
		RandomAccessFile raf = null;
		long alreadyWrite = 0;
		FileInputStream fis = null;
		int len = 0;
		byte[] bt = new byte[1024];
		try {
			raf = new RandomAccessFile(saveDirectory + "//" + fileRealName,
					"rw");
			for (int i = 0; i < FileNum; i++) {
				raf.seek(alreadyWrite);
				// System.out.println("alreadyWrite:"+alreadyWrite);
				fis = new FileInputStream(srcDirectory + "//"
						+ separatedFilesAndSize[i][0]);
				while ((len = fis.read(bt)) > 0) {
					raf.write(bt, 0, len);
				}
				fis.close();
				alreadyWrite = alreadyWrite
						+ Long.parseLong(separatedFilesAndSize[i][1]);
			}
			raf.close();
		} catch (Exception e) {
			e.printStackTrace();
			try {
				if (raf != null)
					raf.close();
				if (fis != null)
					fis.close();
			} catch (IOException f) {
				f.printStackTrace();
			}
			return false;
		}
		return true;
	}

	public void deleteTmp() {
		for (int i = 0; i < FileNum; i++) {
			File file = new File(srcDirectory + "//"
					+ separatedFilesAndSize[i][0]);
			file.delete();
		}
		File file1 = new File(srcDirectory);
		file1.delete();
	}

	public void run() {
		getFileAttribute(srcDirectory);
		CombFile();
		deleteTmp();
	}
}



代码测试:

第一次下载

part1: 0--->4054021, size: 4054021
part2: 4054021--->8108042, size: 4054021
part3: 8108042--->12162063, size: 4054021
part4: 12162063--->16216084, size: 4054021
part5: 16216084--->20270105, size: 4054021
part6: 20270105--->24324126, size: 4054021
part7: 24324126--->28378147, size: 4054021
part8: 28378147--->32432168, size: 4054021
part9: 32432168--->36486190, size: 4054022
线程3正在接收文件...
线程2正在接收文件...
线程9正在接收文件...
线程6正在接收文件...
线程7正在接收文件...
线程4正在接收文件...
线程1正在接收文件...
线程5正在接收文件...
线程8正在接收文件...

第二次下载

part1: 0--->4054021, size: 4054021
part2: 4054021--->8108042, size: 4054021
part3: 8108042--->12162063, size: 4054021
线程1文件正在续传...
线程2文件正在续传...
part4: 12162063--->16216084, size: 4054021
part5: 16216084--->20270105, size: 4054021
线程3文件正在续传...
part6: 20270105--->24324126, size: 4054021
线程5文件正在续传...
part7: 24324126--->28378147, size: 4054021
part8: 28378147--->32432168, size: 4054021
线程6文件正在续传...
part9: 32432168--->36486190, size: 4054022
线程7文件正在续传...
线程4文件正在续传...
线程9文件正在续传...
线程8文件正在续传...
线程5文件续传完成...
线程9文件续传完成...
线程7文件续传完成...
线程6文件续传完成...
线程3文件续传完成...
线程2文件续传完成...
线程1文件续传完成...
线程4文件续传完成...
线程8文件续传完成...