詳解Java多線程和IO流的應(yīng)用
Java多線程和流的應(yīng)用
最近看到了一個(gè)例子,是使用多線程的方式下載文件,感覺很有趣,探索了一下,并且嘗試了使用多線程進(jìn)行本地復(fù)制文件。寫完之后,發(fā)現(xiàn)了這兩個(gè)其實(shí)很相似,無論是本地文件復(fù)制,還是網(wǎng)絡(luò)多線程下載,對(duì)于流的使用都是一樣的。對(duì)于本地文件系統(tǒng)來說,輸入流就是從本地文件系統(tǒng)的一個(gè)文件來獲取,對(duì)于網(wǎng)絡(luò)資源來說,是從遠(yuǎn)處服務(wù)器上的一個(gè)文件來獲取。
注: 雖然這個(gè)多線程下載的代碼,很多人都寫過了,不過應(yīng)該不是所有人都能理解吧,我這里就再寫一遍,哈。
使用多線程的一個(gè)顯而易見的好處就是:利用空閑的 CPU,加快速度。 但是注意不是線程越多越好,雖然好像n個(gè)線程一起下載,每個(gè)線程下載一小部分,下載時(shí)間就會(huì)變?yōu)?/n 了。這是很淺顯的認(rèn)識(shí),就好像一個(gè)人蓋一個(gè)房子需要100天,難道10000個(gè)人,只需要1/10 天一樣了?(這是一個(gè)夸張的說法,哈哈!)
線程之間的切換也是需要系統(tǒng)開銷的,線程的數(shù)目還是要控制在一個(gè)合理的范圍內(nèi)才行。
RamdomAccessFile
這個(gè)類比較獨(dú)特,它即可以從文件中讀取數(shù)據(jù),也可以向文件中寫入數(shù)據(jù)。但是它不是 OutputStream 和 InputStream 的子類,它是實(shí)現(xiàn)了這兩個(gè)接口 DataOutput 、DataInput 的一個(gè)類。
API 中的介紹:
該類的實(shí)例支持讀取和寫入隨機(jī)訪問文件。 隨機(jī)訪問文件的行為類似于存儲(chǔ)在文件系統(tǒng)中的大量字節(jié)。 有一種游標(biāo),或索引到隱含的數(shù)組,稱為文件指針 ; 輸入操作讀取從文件指針開始的字節(jié),并使文件指針超過讀取的字節(jié)。 如果在讀/寫模式下創(chuàng)建隨機(jī)訪問文件,則輸出操作也可用; 輸出操作從文件指針開始寫入字節(jié),并將文件指針提前到寫入的字節(jié)。 寫入隱式數(shù)組的當(dāng)前端的輸出操作會(huì)導(dǎo)致擴(kuò)展數(shù)組。 文件指針可以通過讀取getFilePointer方法和由設(shè)置seek方法。
所以,這個(gè)類最重要的就是 seek 方法了,使用 seek 方法,可以控制寫入的位置,所以實(shí)現(xiàn)多線程就容易的多了。因此,無論是本地文件復(fù)制,還是網(wǎng)絡(luò)多線程下載都需要這個(gè)類的作用。
具體思路是: 首先使用 RandomAccessFile 創(chuàng)建一個(gè) File 對(duì)象,然后設(shè)置這個(gè)文件的大小。(對(duì)的,它可以直接設(shè)置文件的大小。)將這個(gè)文件設(shè)置成和要復(fù)制或下載的文件一樣的。(雖然我們并沒有向這個(gè)文件中寫入數(shù)據(jù),但是這個(gè)文件已經(jīng)創(chuàng)建了。)將文件分為若干部分,使用線程復(fù)制或者下載每一部分的內(nèi)容。
這有點(diǎn)類似文件的覆蓋,如果一個(gè)已經(jīng)存在的文件,從這個(gè)文件的頭部開始寫入數(shù)據(jù),一直寫到文件的尾部,那么原來的文件就不存在了,變成了寫入的新文件。
設(shè)置文件大小:
private static void createOutputFile(File file, long length) throws IOException {
try (
RandomAccessFile raf = new RandomAccessFile(file, "rw")){
raf.setLength(length);
}
}
用圖片來說明: 這個(gè)圖表示一個(gè) 8191 字節(jié)大小的文件: 每一個(gè)部分大小是:8191 / 4 = 2047字節(jié)
將這個(gè)文件的分為四個(gè)部分,每一個(gè)部分使用一個(gè)線程進(jìn)行復(fù)制或下載,其中每一個(gè)箭頭代表一個(gè)線程的開始下載位置。我特意將最后一個(gè)部分,沒有設(shè)置為 1024 byte,這是因?yàn)槲募苌偈钦媚鼙?1024 byte 整除的。(之所以使用 1024 byte,是因?yàn)槲颐看螘?huì)讀取 1024 byte,如果讀取到 1024 byte的話, 否則寫入讀取到的相應(yīng)字節(jié)數(shù))。
按照這個(gè)示意圖,每一個(gè)線程下載 2047 byte,那么總共下載的字節(jié)數(shù)是:2047 * 4 = 8188 字節(jié) < 8191 字節(jié)(文件的總大?。?/strong> 所以這就產(chǎn)生了一個(gè)問題,下載的字節(jié)數(shù)少于總字節(jié)數(shù),這就是問題了,所以必須要下載的字節(jié)數(shù)大于總字節(jié)數(shù)才行。(多了沒有關(guān)系,因?yàn)槎嘞螺d的部分,會(huì)被后面的給覆蓋掉,不會(huì)產(chǎn)生了問題。)
所以每個(gè)部分的大小應(yīng)該是:8191 / 4 + 1 = 2048 字節(jié)。(這樣四部分的大小相加是超過總大小的,不會(huì)發(fā)生數(shù)據(jù)的丟失問題。)
所以,這里這個(gè)加 1 是很有必要的。
long size = len / FileCopyUtil.THREAD_NUM + 1;

每個(gè)線程下載完成的位置(右邊) 每個(gè)線程,只復(fù)制下載自己的那部分,所以不需要全部下載完所有的內(nèi)容,所以讀取文件數(shù)據(jù)并寫入文件的部分,會(huì)多加一個(gè)判斷。
這里增加一個(gè)計(jì)數(shù)器:curlen。它表示是當(dāng)前復(fù)制或者下載的長(zhǎng)度,然后每次讀取后和 size(每部分的大小)進(jìn)行比較,如果 curlen 大于 size 就表示相應(yīng)的部分下載完成了(當(dāng)然了,這些都要在數(shù)據(jù)沒有讀取完的條件下判斷)。
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(targetFile));
RandomAccessFile raf = new RandomAccessFile(outputFile, "rw")){
bis.skip(position);
raf.seek(position);
int hasRead = 0;
byte[] b = new byte[1024];
long curlen = 0;
while(curlen < size && (hasRead = bis.read(b)) != -1) {
raf.write(b, 0, hasRead);
curlen += (long)hasRead;
}
System.out.println(n+" "+position+" "+curlen+" "+size);
} catch (IOException e) {
e.printStackTrace();
}

還有需要注意的是,每個(gè)線程下載的時(shí)候都要: 1. 輸出流設(shè)置文件指針的位置。 2. 輸入流跳過不需要讀取的字節(jié)。
這是很重要的一步,應(yīng)該是很好理解的。
bis.skip(position); raf.seek(position);
多線程本地文件復(fù)制(完整代碼)
package dragon;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
* 用于進(jìn)行文件復(fù)制,但不是常規(guī)的文件復(fù)制 。
* 準(zhǔn)備仿照瘋狂Java,寫一個(gè)多線程的文件復(fù)制工具。
* 即可以本地復(fù)制和網(wǎng)絡(luò)復(fù)制
* */
/**
* 設(shè)計(jì)思路:
* 獲取目標(biāo)文件的大小,然后設(shè)置復(fù)制文件的大?。ㄟ@樣做是有好處的),
* 然后使用將文件分為 n 分,使用 n 個(gè)線程同時(shí)進(jìn)行復(fù)制(這里我將 n 取為 4)。
*
* 可以進(jìn)一步拓展:
* 加強(qiáng)為斷點(diǎn)復(fù)制功能,即程序中斷以后,
* 仍然可以繼續(xù)從上次位置恢復(fù)復(fù)制,減少不必要的重復(fù)開銷
* */
public class FileCopyUtil {
//設(shè)置一個(gè)常量,復(fù)制線程的數(shù)量
private static final int THREAD_NUM = 4;
private FileCopyUtil() {}
/**
* @param targetPath 目標(biāo)文件的路徑
* @param outputPath 復(fù)制輸出文件的路徑
* @throws IOException
* */
public static void transferFile(String targetPath, String outputPath) throws IOException {
File targetFile = new File(targetPath);
File outputFilePath = new File(outputPath);
if (!targetFile.exists() || targetFile.isDirectory()) { //目標(biāo)文件不存在,或者是一個(gè)文件夾,則拋出異常
throw new FileNotFoundException("目標(biāo)文件不存在:"+targetPath);
}
if (!outputFilePath.exists()) { //如果輸出文件夾不存在,將會(huì)嘗試創(chuàng)建,創(chuàng)建失敗,則拋出異常。
if(!outputFilePath.mkdir()) {
throw new FileNotFoundException("無法創(chuàng)建輸出文件:"+outputPath);
}
}
long len = targetFile.length();
File outputFile = new File(outputFilePath, "copy"+targetFile.getName());
createOutputFile(outputFile, len); //創(chuàng)建輸出文件,設(shè)置好大小。
long[] position = new long[4];
//每一個(gè)線程需要復(fù)制文件的起點(diǎn)
long size = len / FileCopyUtil.THREAD_NUM + 1;
for (int i = 0; i < FileCopyUtil.THREAD_NUM; i++) {
position[i] = i*size;
copyThread(i, position[i], size, targetFile, outputFile);
}
}
//創(chuàng)建輸出文件,設(shè)置好大小。
private static void createOutputFile(File file, long length) throws IOException {
try (
RandomAccessFile raf = new RandomAccessFile(file, "rw")){
raf.setLength(length);
}
}
private static void copyThread(int i, long position, long size, File targetFile, File outputFile) {
int n = i; //Lambda 表達(dá)式的限制,無法使用變量。
new Thread(()->{
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(targetFile));
RandomAccessFile raf = new RandomAccessFile(outputFile, "rw")){
bis.skip(position); //跳過不需要讀取的字節(jié)數(shù),注意只能先后跳
raf.seek(position); //跳到需要寫入的位置,沒有這句話,會(huì)出錯(cuò),但是很難改。
int hasRead = 0;
byte[] b = new byte[1024];
/**
* 注意,每個(gè)線程只是讀取一部分?jǐn)?shù)據(jù),不能只以 -1 作為循環(huán)結(jié)束的條件
* 循環(huán)退出條件應(yīng)該是兩個(gè),即寫入的字節(jié)數(shù)大于需要讀取的字節(jié)數(shù) 或者 文件讀取結(jié)束(最后一個(gè)線程讀取到文件末尾)
*/
long curlen = 0;
while(curlen < size && (hasRead = bis.read(b)) != -1) {
raf.write(b, 0, hasRead);
curlen += (long)hasRead;
}
System.out.println(n+" "+position+" "+curlen+" "+size);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
多線程網(wǎng)絡(luò)下載(完整代碼)
package dragon;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URL;
import java.net.URLConnection;
/*
* 多線程下載文件:
* 通過一個(gè) URL 獲取文件輸入流,使用多線程技術(shù)下載這個(gè)文件。
* */
public class FileDownloadUtil {
//下載線程數(shù)
private static final int THREAD_NUM = 4;
/**
* @param url 資源位置
* @param output 輸出路徑
* @throws IOException
* */
public static void transferFile(String url, String output) throws IOException {
init(output);
URL resource = new URL(url);
URLConnection connection = resource.openConnection();
//獲取文件類型
String type = connection.getContentType();
if (type != null) {
type = "."+type.split("/")[1];
} else {
type = "";
}
//創(chuàng)建文件,并設(shè)置長(zhǎng)度。
long len = connection.getContentLength();
String filename = System.currentTimeMillis()+type;
try (RandomAccessFile raf = new RandomAccessFile(new File(output, filename), "rw")){
raf.setLength(len);
}
//為每一個(gè)線程分配相應(yīng)的下載其實(shí)位置
long size = len / THREAD_NUM + 1;
long[] position = new long[THREAD_NUM];
File downloadFile = new File(output, filename);
//開始下載文件: 4個(gè)線程
download(url, downloadFile, position, size);
}
private static void download(String url, File file, long[] position, long size) throws IOException {
//開始下載文件: 4個(gè)線程
for (int i = 0 ; i < THREAD_NUM; i++) {
position[i] = i * size; //每一個(gè)線程下載的起始位置
int n = i; // Lambda 表達(dá)式的限制,無法使用變量
new Thread(()->{
URL resource = null;
URLConnection connection = null;
try {
resource = new URL(url);
connection = resource.openConnection();
} catch (IOException e) {
e.printStackTrace();
}
try (
BufferedInputStream bis = new BufferedInputStream(connection.getInputStream());
RandomAccessFile raf = new RandomAccessFile(file, "rw")){ //每個(gè)流一旦關(guān)閉,就不能打開了
raf.seek(position[n]); //跳到需要下載的位置
bis.skip(position[n]); //跳過不需要下載的部分
int hasRead = 0;
byte[] b = new byte[1024];
long curlen = 0;
while(curlen < size && (hasRead = bis.read(b)) != -1) {
raf.write(b, 0, hasRead);
curlen += (long)hasRead;
}
System.out.println(n+" "+position[n]+" "+curlen+" "+size);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}) .start();
}
}
private static void init(String output) throws FileNotFoundException {
File path = new File(output);
if (!path.exists()) {
if (!path.mkdirs()) {
throw new FileNotFoundException("無法創(chuàng)建輸出路徑:"+output);
}
} else if (path.isFile()) {
throw new FileNotFoundException("輸出路徑不是一個(gè)目錄:"+output);
}
}
}
測(cè)試代碼及結(jié)果
因?yàn)檫@個(gè)多線程文件復(fù)制和多線程下載是很相似的,所以就放在一起測(cè)試了。我也想將兩個(gè)寫在一個(gè)類里面,這樣可以做成方法的重載調(diào)用。 文件復(fù)制的第一個(gè)參數(shù)可以是 String 或者 URI。 使用這個(gè)作為目標(biāo)文件的參數(shù)。
public File(URI uri)
網(wǎng)絡(luò)文件下載的第一個(gè)參數(shù),可以使用 String 或者是 URL。 不過,因?yàn)橄葘懙倪@個(gè)文件復(fù)制,后寫的多線程下載,就沒有做這部分。不過現(xiàn)在這樣功能也達(dá)到了,可以進(jìn)行本地文件的復(fù)制(多線程)和網(wǎng)絡(luò)文件的下載(多線程)。
package dragon;
import java.io.IOException;
public class FileCopyTest {
public static void main(String[] args) throws IOException {
//復(fù)制文件
long start = System.currentTimeMillis();
try {
FileCopyUtil.transferFile("D:\\DB\\download\\timg.jfif", "D:\\DBC");
} catch (IOException e) {
e.printStackTrace();
}
long time = System.currentTimeMillis()-start;
System.out.println("time: "+time);
//下載文件
start = System.currentTimeMillis();
FileDownloadUtil.transferFile("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1578151056184&di=594a34f05f3587c31d9377a643ddd72e&imgtype=0&src=http%3A%2F%2Fn.sinaimg.cn%2Fsinacn%2Fw1600h1000%2F20180113%2F0bdc-fyqrewh5850115.jpg", "D:\\DB\\download");
System.out.println("time: "+(System.currentTimeMillis()-start));
}
}
運(yùn)行截圖: 注意:這里這個(gè)時(shí)間并不是復(fù)制和下載需要的時(shí)間,實(shí)際上它沒有這個(gè)功能!
注意:雖然兩部分代碼是相同的,但是第三列數(shù)字,卻不是完全相同的,這個(gè)似乎是因?yàn)楸镜睾途W(wǎng)絡(luò)得區(qū)別吧。但是最后得文件是完全相同的,沒有問題得。(我本地文件復(fù)制得是網(wǎng)絡(luò)下載得那張圖片,使用圖片進(jìn)行測(cè)試有一個(gè)好處,就是如果錯(cuò)了一點(diǎn)(字節(jié)數(shù)目不對(duì)),這個(gè)圖片基本上就會(huì)產(chǎn)生問題。)

產(chǎn)生錯(cuò)誤之后的圖片: 圖片無法正常顯示,會(huì)出現(xiàn)很多的問題,這就說明一定是代碼寫錯(cuò)了,哈哈。不過代碼的 bug 有時(shí)候還是很費(fèi)時(shí)間的。

總結(jié)
多線程復(fù)制和多線程下載,對(duì)于IO 流的使用都是相同的,所以掌握好IO 流的使用是很關(guān)鍵的一步,這樣就可以做很多很有趣的事情了。將IO流結(jié)合線程和網(wǎng)絡(luò)是一片廣闊的天地,但是我也是最近才看了一點(diǎn)網(wǎng)絡(luò)的知識(shí)。(網(wǎng)絡(luò)是很難的,寫這個(gè)多線程下載就遇到了一些網(wǎng)絡(luò)類上面的問題。)
使用計(jì)時(shí)器實(shí)現(xiàn)了多線程復(fù)制文件的斷點(diǎn)復(fù)制功能,感興趣的可以了解一下。 多線程斷點(diǎn)復(fù)制
到此這篇關(guān)于詳解Java多線程和IO流的應(yīng)用的文章就介紹到這了,更多相關(guān)Java多線程和IO流內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java字符串拼接+和StringBuilder的比較與選擇
Java 提供了兩種主要的方式:使用 "+" 運(yùn)算符和使用 StringBuilder 類,本文主要介紹了Java字符串拼接+和StringBuilder的比較與選擇,感興趣的可以了解一下2023-10-10
關(guān)于springBoot yml文件的list讀取問題總結(jié)(親測(cè))
這篇文章主要介紹了關(guān)于springBoot yml文件的list讀取問題總結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
一個(gè)處理用戶登陸的servlet簡(jiǎn)單實(shí)例
這篇文章主要介紹了一個(gè)處理用戶登陸的servlet簡(jiǎn)單實(shí)例,可通過servlet實(shí)現(xiàn)處理用戶登錄的功能,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-01-01
springboot使用外置tomcat啟動(dòng)方式
這篇文章主要介紹了springboot使用外置tomcat啟動(dòng)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
Java 如何使用JDBC連接數(shù)據(jù)庫(kù)
這篇文章主要介紹了Java 如何使用JDBC連接數(shù)據(jù)庫(kù),幫助大家更好的理解和學(xué)習(xí)使用Java,感興趣的朋友可以了解下2021-02-02
Java 動(dòng)態(tài)加載jar和class文件實(shí)例解析
這篇文章主要介紹了Java 動(dòng)態(tài)加載jar和class文件實(shí)例解析,分享了相關(guān)代碼示例,小編覺得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-02-02
springboot整合Excel填充數(shù)據(jù)代碼示例
這篇文章主要給大家介紹了關(guān)于springboot整合Excel填充數(shù)據(jù)的相關(guān)資料,文中通過代碼示例介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用springboot具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-08-08

