【OS大作業】用多執行緒統計txt檔案中字元個數(Java實現)
阿新 • • 發佈:2018-11-17
問題描述
給定一個txt檔案,利用不同個數的執行緒查詢檔案中某字元的個數,探究執行緒個數與查詢時間的關係。
本作業程式碼使用JAVA實現,版本為10.0.2,使用的IDE為Eclipse4.9.0. 結果測試所用的txt檔案內容為英文,編碼格式為UTF-8。
原始碼
第一版程式碼:(僅支援單執行緒、按行讀取、可以讀取字串/字元,速度快)
package searchtxt; //包名稱 import java.io.BufferedReader; //緩衝字元輸入流 import java.io.File; import java.io.FileReader; import java.io.IOException; /* * * 讀取一txt文件,每次讀取一行,用BufferedReader(FileReader fr) * */ public class demo { static int totalCount; //待查詢關鍵字的個數 static String key = "a"; //帶查詢關鍵字字串 public static void main(String[] args) throws IOException { Thread1 mTh1=new Thread1(); //建立一個執行緒 mTh1.setTotalCount(0); //傳參,關鍵字個數初始化為0 mTh1.setKey(key); //傳入要查詢的關鍵字 mTh1.start(); //開啟執行緒,執行run方法 totalCount=mTh1.getTotalCount(); //獲取該執行緒查詢結果 } } class Thread1 extends Thread{ //繼承自Thread類 private int totalCount; //關鍵字個數 private String key; //關鍵字字串 @SuppressWarnings("resource") public void run() { File f = new File("src/OneHundredYearsofSolitude.txt"); //待查詢檔案路徑 FileReader fr; //該類按字元讀取流中資料 String str; try { long startTime=System.currentTimeMillis(); //獲取開始時間 fr = new FileReader(f); BufferedReader br = new BufferedReader(fr); //開始讀取檔案直到末尾 while ((str = br.readLine()) != null) { //將每次讀取的資料放入str字串中,在其中查詢關鍵字key的個數加入totalcount setTotalCount(getTotalCount() + countKey(str, key)); } long endTime=System.currentTimeMillis(); //獲取結束時間 //輸出結果 System.out.println("文章中一共出現了:" + key + ":" + totalCount + "次"); System.out.println("程式執行時間: "+(endTime-startTime)+"ms"); } catch (IOException e1) { e1.printStackTrace(); } } //該方法從str中查詢key,返回個數 public static int countKey(String str, String key){ int index = 0; int count = 0; while ((index = str.indexOf(key, index)) != -1) { index += key.length(); count++; } return count; } public int getTotalCount() { return totalCount; } public void setTotalCount(int totalCount) { this.totalCount = totalCount; } public void setKey(String key) { this.key = key; } }
第二版程式碼:(可自行選擇匯流排程個數,將檔案分塊讓各個執行緒按字元查詢)
1、MultiReadTest.java(主程式)
package searchtxt; import java.io.File; import java.io.RandomAccessFile; //用於讀寫檔案 import java.util.concurrent.CountDownLatch; //CountDownLatch類,用於執行緒同步 /* * * 用n個執行緒讀取txt檔案,當獲取到指定關鍵字時,在指定的物件加1 * */ public class MultiReadTest { @SuppressWarnings("resource") public static void main(String[] args) { //開始時間設為0 long startTime=0; //結束時間設為0 long endTime=0; /* //可手動輸入執行緒數目,除錯時註釋掉 Scanner input= new Scanner(System.in); //為Scanner例項化物件input int n=input.nextInt(); //掃描控制檯輸入 final int DOWN_THREAD_NUM = n; */ // //指定執行緒數目 //final成員變數必須在宣告的時候初始化或在構造方法中初始化,不能再次賦值。 final int DOWN_THREAD_NUM = 8; // //要讀取的txt檔案路徑 final String OUT_FILE_NAME = "src/8MB.txt"; //要查詢的關鍵字 final String keywords = "a"; //CountDownLatch類位於java.util.concurrent包下,利用它可以實現類似計數器的功能。 //具體使用方法為: //CountDownLatch的建構函式接收一個int型別的引數作為計數器,如果你想等待N個點完成,這裡就傳入N。 //當我們呼叫一次CountDownLatch的countDown方法時,N就會減1,CountDownLatch的await會阻塞當前執行緒,直到N變成零。 //在這裡,我們設定CountDownLatch的值為DOWN_THREAD_NUM CountDownLatch doneSignal = new CountDownLatch(DOWN_THREAD_NUM); //RandomAccessFile是Java輸入/輸出流體系中功能最豐富的檔案內容訪問類,可以讀取檔案內容,也可以向檔案輸出資料 //與普通的輸入/輸出流不同的是,RandomAccessFile支援跳到檔案任意位置讀寫資料 //RandomAccessFile物件包含一個記錄指標,用以標識當前讀寫處的位置 //當程式建立一個新的RandomAccessFile物件時,該物件的檔案記錄指標對於檔案頭(也就是0處) //當讀寫n個位元組後,檔案記錄指標將會向後移動n個位元組 //除此之外,RandomAccessFile可以自由移動該記錄指標 RandomAccessFile[] outArr = new RandomAccessFile[DOWN_THREAD_NUM]; try{ //此方法用於獲取檔案長度,最大隻能獲取2g的檔案大小,因為返回值型別為long long length = new File(OUT_FILE_NAME).length(); //輸出檔案長度 System.out.println("檔案總長度:"+length+"位元組,即"+length/1024/1024+"MB"); //計算每個執行緒應該讀取的位元組數 long numPerThred = length / DOWN_THREAD_NUM; System.out.println("共有"+DOWN_THREAD_NUM+"個執行緒,每個執行緒讀取的位元組數:"+numPerThred+"位元組"); //計算整個檔案整除後剩下的餘數 long left = length % DOWN_THREAD_NUM; //獲取開始時間 startTime=System.currentTimeMillis(); //為每個執行緒開啟一個輸入流、一個RandomAccessFile物件 //讓每個執行緒分別負責讀取檔案的不同部分 for (int i = 0; i < DOWN_THREAD_NUM; i++) { //rw:以讀取、寫入方式開啟指定檔案 outArr[i] = new RandomAccessFile(OUT_FILE_NAME, "rw"); //最後一個執行緒讀取指定numPerThred+left個位元組 if (i == DOWN_THREAD_NUM - 1) { //輸出其要讀的位元組範圍(測試時應把這句註釋掉,因為會影響執行時間的測定) //System.out.println("第"+i+"個執行緒讀取從"+i * numPerThred+"到"+((i + 1) * numPerThred+ left)+"的位置"); //ReadThread類用於讀取檔案,在讀取到關鍵字時,在指定的變數加一 new ReadThread(i * numPerThred, (i + 1) * numPerThred + left, //開始位置和結束位置 outArr[i], //第i個RandomAccessFile物件 keywords, //關鍵詞 doneSignal //CountDownLatch類 ).start(); //執行緒啟動 } //每個執行緒負責讀取一定的numPerThred個位元組 else { //輸出其要讀的位元組範圍(測試時應把這句註釋掉,因為會影響執行時間的測定) //System.out.println("第"+i+"個執行緒讀取從"+i * numPerThred+"到"+((i + 1) * numPerThred)+"的位置"); new ReadThread(i * numPerThred, (i + 1) * numPerThred-1, outArr[i], keywords, doneSignal ).start(); } } }catch(Exception e){ e.printStackTrace(); //捕獲異常 } try { //確認所有執行緒任務完成,開始執行主執行緒的操作 doneSignal.await(); //獲取結束時間 endTime=System.currentTimeMillis(); } catch (InterruptedException e) { e.printStackTrace(); } //獲取關鍵字的計數值 KeyWordsCount k = KeyWordsCount.getCountObject(); System.out.println("指定關鍵字"+keywords+"出現的次數:"+k.getCount()); System.out.println("程式執行時間:"+(endTime-startTime)+"ms"); } }
2、KeyWordsCount.java(統計關鍵字的物件)
package searchtxt; /** * 統計關鍵字的物件 */ public class KeyWordsCount { //用於類的呼叫 private static KeyWordsCount kc; //總關鍵字個數 private int count = 0; //返回類 public static synchronized KeyWordsCount getCountObject(){ //若還沒有則建立 if(kc == null){ kc = new KeyWordsCount(); } //返回本類 return kc; } //執行緒呼叫本方法將自己統計的個數加入總個數 public synchronized void addCount(String str, int count){ //System.out.println(str+"執行緒增加了關鍵字次數:"+count); this.count += count; } public int getCount() { return count; } public void setCount(int count) { this.count = count; } }
3、ReadThread.java(執行緒的實現)
package searchtxt;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.concurrent.CountDownLatch;
/**
* 這個執行緒用來讀取檔案,當獲取到指定關鍵字時,在指定的物件加1
**/
public class ReadThread extends Thread{
//定義位元組陣列的長度
private final int BUFF_LEN = 1;
//定義讀取的起始點
private long start;
//定義讀取的結束點
private long end;
//將讀取到的位元組輸出到raf中,randomAccessFile可以理解為檔案流
private RandomAccessFile raf;
//執行緒中需要指定的關鍵字
private String keywords;
//此執行緒讀到關鍵字的次數
private int curCount = 0;
//用於確認所有執行緒計數完成的計數類
private CountDownLatch doneSignal;
//建構函式
public ReadThread(long start, long end, RandomAccessFile raf, String keywords, CountDownLatch doneSignal){
this.start = start; //讀取開始位置
this.end = end; //讀取結束位置
this.raf = raf; //第i個RandomAccessFile物件,將讀取到的位元組輸出到raf中
this.keywords = keywords; //關鍵字
this.doneSignal = doneSignal; //計數類
}
//執行緒功能:計數
public void run(){
try {
//RandomAccessFile物件
//void seek(long pos):將檔案記錄指標定位到pos位置
raf.seek(start);
//計算本執行緒負責讀取檔案部分的長度
long contentLen = end - start;
//BUFF_LEN為位元組陣列的長度
//計算最多需要讀取幾次就可以完成本執行緒的讀取
long times = contentLen / BUFF_LEN+1;
//輸出需要讀的次數
//System.out.println(this.toString() + " 需要讀的次數:"+times);
//位元組陣列
byte[] buff = new byte[BUFF_LEN];
int hasRead = 0;
String result = null;
//遍歷每次讀取
for (int i = 0; i < times; i++) {
//之前SEEK指定了起始位置,這裡用raf.read方法讀入指定位元組組buff長度的內容
//返回值為讀取到的位元組數
hasRead = raf.read(buff);
//小於0,則退出迴圈(到了位元組陣列的末尾)
if (hasRead < 0) {
break;
}
//取出讀取的buff位元組陣列內容
result = new String(buff,"utf-8");
//System.out.println(result);
//計算本次讀取中關鍵字的個數並累加
int count = this.getCountByKeywords(result, keywords);
if(count > 0){
this.curCount += count;
}
}
//將本執行緒讀取的關鍵字個數加入總關鍵字個數
KeyWordsCount kc = KeyWordsCount.getCountObject();
kc.addCount(this.toString(), this.curCount);
//本執行緒執行完畢,N--
doneSignal.countDown();
} catch (IOException e) {
e.printStackTrace();
}
}
public int getCountByKeywords(String statement, String key){
/*
//split函式是用於按指定字元(串)或正則去分割某個字串,結果以字串陣列形式返回
//.length便是分割的數目,再-1是指定字串的數目
return statement.split(key).length-1;
*/
int count = 0;
int index = 0;
while( ( index = statement.indexOf(key, index) ) != -1 )
{
index = index+key.length();
count++;
}
return count;
}
public long getStart() {
return start;
}
public void setStart(long start) {
this.start = start;
}
public long getEnd() {
return end;
}
public void setEnd(long end) {
this.end = end;
}
public RandomAccessFile getRaf() {
return raf;
}
public void setRaf(RandomAccessFile raf) {
this.raf = raf;
}
public int getCurCount() {
return curCount;
}
public void setCurCount(int curCount) {
this.curCount = curCount;
}
public CountDownLatch getDoneSignal() {
return doneSignal;
}
public void setDoneSignal(CountDownLatch doneSignal) {
this.doneSignal = doneSignal;
}
}
結果分析
針對每個執行緒數目做十組測試,去掉最小值和最大值,取平均值畫折線圖,資料和圖表如下所示。
執行緒數/時間ms | 1 | 2 | 4 | 5 | 6 | 7 | 8 | 16 | 32 | 64 |
1 | 4148 | 2317 | 1243 | 1254 | 1261 | 1246 | 1256 | 1279 | 1255 | 1288 |
2 | 4115 | 2255 | 1276 | 1245 | 1244 | 1248 | 1238 | 1275 | 1309 | 1283 |
3 | 4142 | 2257 | 1253 | 1244 | 1340 | 1233 | 1241 | 1264 | 1297 | 1297 |
4 | 4094 | 2296 | 1254 | 1264 | 1228 | 1282 | 1302 | 1288 | 1266 | 1306 |
5 | 4275 | 2240 | 1275 | 1255 | 1265 | 1268 | 1253 | 1265 | 1264 | 1307 |
6 | 4121 | 2295 | 1269 | 1261 | 1263 | 1254 | 1299 | 1256 | 1282 | 1316 |
7 | 4224 | 2276 | 1233 | 1351 | 1244 | 1239 | 1253 | 1274 | 1277 | 1302 |
8 | 4092 | 2316 | 1288 | 1280 | 1255 | 1347 | 1232 | 1271 | 1283 | 1296 |
9 | 4096 | 2280 | 1274 | 1267 | 1263 | 1272 | 1251 | 1284 | 1289 | 1289 |
10 | 4187 | 2292 | 1286 | 1250 | 1269 | 1279 | 1263 | 1408 | 1280 | 1280 |
最小值 | 4092 | 2240 | 1233 | 1244 | 1228 | 1233 | 1232 | 1256 | 1255 | 1280 |
最大值 | 4275 | 2317 | 1288 | 1351 | 1340 | 1347 | 1302 | 1408 | 1309 | 1316 |
平均值 | 4140.875 | 2283.375 | 1266.25 | 1259.5 | 1258 | 1261 | 1256.75 | 1275 | 1279.75 | 1296 |
由上圖可以看出,當執行緒數目小於4個時,執行緒數目每翻一倍,用時約減少50%,之後隨著執行緒數目的增長,用時趨平,在8個執行緒時達到最低點,此後緩慢上升。
結果解釋:在一定程度內增加執行緒數目會提高系統併發度,減少讀取磁碟檔案的時間開銷,緩解IO速度過慢而CPU速度極快的矛盾,從而能夠大幅度地提高時間方面的效能;但執行緒數目過多時,切換執行緒所需開銷也逐漸增大,此時反而會增加任務用時,得不償失。
【參考博文】
感謝大神們的無私奉獻,讓沒學過JAVA的小白也能完成大作業,程式碼經過一定修改,註釋均由百度百科和CSDN查詢得來,如有錯誤請務必指出。