问题描述
给定一个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查找得来,如有错误请务必指出。