【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查找得来,如有错误请务必指出。

猜你喜欢

转载自blog.csdn.net/qq_41727666/article/details/84111318