1. 程式人生 > >【OS大作業】用多執行緒統計txt檔案中字元個數(Java實現)

【OS大作業】用多執行緒統計txt檔案中字元個數(Java實現)

問題描述

給定一個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速度極快的矛盾,從而能夠大幅度地提高時間方面的效能;但執行緒數目過多時,切換執行緒所需開銷也逐漸增大,此時反而會增加任務用時,得不償失。


【參考博文】

1、JAVA多執行緒讀寫檔案範例

2、java獲取程式執行時間

感謝大神們的無私奉獻,讓沒學過JAVA的小白也能完成大作業,程式碼經過一定修改,註釋均由百度百科和CSDN查詢得來,如有錯誤請務必指出。