坚持打卡!
这个主要实现的是,多线程处理大文件,这里的大文件指的是好几十M的文件,例如我下边写的处理几百万条数据,对他们进行过滤,得到想要的数据并输出到指定的文件中。
一开始走了不少弯路,我现在讲一下我的主要实现的思路(这里也参考了很多大佬们的意见),当自己写出来的时候才是属于自己的。
主要思路:
1,创建线程池,其多个线程。去并发的读取同一个文件
2,我这里线程的实现是通过实现Callable接口,重写call()
3,既然要实现多线程,并发。所以每一个线程就要负责执行自己的操作,我之前存在一个缺点:虽然我起了很多线程去读,但是这些线程都是共用一个读的对象,即就是要等待其他线程处理完后才轮到自己,这就是串行,还不如一个单线程去执行。查阅了方法,有一种实现的方式是:对所要读取的文件进行按字节拆分,分块(区域)。即有多少个线程就把源文件分割成多少块(区域),每个线程就只对自己所负责的块进行操作,这样就互不影响了。
4,RandomAccessFile 这个就是上面实现的核心类,大家可以去仔细查阅下,我目前了解还不是很深入。下面讲的地方如果不正确,还请大佬们多多指正。
5,分块就涉及一个问题,分割的时候。切点就切在了一行数据的中间,怎么办?
下雨了我要 | 回家收衣服了。 |
所以我们要对这个起始位置,和末尾位置的字符进行判断,是否是换行符:\r\n , \r , \n
不是的话,我们要实现一种递归,去找到当行的起始位置,这样就可以对这一整行进行完整的输出了。
6,把结果输出到指定文件中,这时候就可以使用RandomAccessFile 的write 方法。write()方法在调用结束之后自动移动文件指针,所以你不需要频繁地把指针移动到下一个将要写入数据的位置。所以最终得到处理后的结果数据是无序的。
(我也测试过,每个线程自己输出到一个临时文件,最终再合并成一个一个总文件,并删除临时文件。这样的结果是有序的,但是感觉有些画蛇添足。不知道各位有没有更好的实现方法)
参考代码:
BufferIO.class
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author SanYi&&HLing
*
* 目的:多线程---实现对百万数据---的txt文件进行---处理,并输出到指定的txt文件中
* */
public class BufferIO {
/**
* 核心线程池大小 4
* */
public static final int CORE_POOL_SIZE = 4;
/**
* 最大线程池大小 4
* */
public static final int MAX_MUM_POOL_SIZE = 4;
/**
* 线程最大空闲时间
* */
public static final int KEEP_ALIVE_TIME = 1;
/**
* main 入口类
* */
public static void main(String[] args) throws IOException,
InterruptedException {
// 测试执行时间
long start = System.currentTimeMillis();
// 创建RandomAccessFile 操作文件
RandomAccessFile fileWirter = null;
// 创建线程池对象
ThreadPoolExecutor executor = null;
try {
// 构造线程池对象
executor = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAX_MUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.MINUTES,
new ArrayBlockingQueue<Runnable>(1));
// 多个 tasks
List<CopyFile> tasks = new ArrayList<>();
// 指定输出的文件
fileWirter = new RandomAccessFile("result.txt", "rw");
// 添加执行的线程
for (int i = 0; i < CORE_POOL_SIZE; i++) {
tasks.add(new CopyFile("data.txt", CORE_POOL_SIZE, i + 1,
fileWirter));
}
// 任务批量提交并执行
List<Future<Object>> futures = executor.invokeAll(tasks);
// 获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回
for (Future<Object> future : futures) {
future.get();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭
if (fileWirter != null) {
fileWirter.close();
}
if (executor != null) {
executor.shutdown();
}
}
// 测试执行时间
long end = System.currentTimeMillis();
System.out.println("执行了" + (end - start) + "ms");
}
}
CopyFile.class
import java.io.RandomAccessFile;
import java.util.concurrent.Callable;
/**
* @author SanYi&&HLing
* 通过实现Callable接口,重写 call 方法来实现线程
* */
public class CopyFile implements Callable<Object> {
// 私有化
private String fileName;
private int threadSize = 1;
private int currentBlock = 1;
private RandomAccessFile fileWirter;
/**
* 有参构造方法
*
* @param fileName
* 源文件
* @param threadSize
* 线程数
* @param currentBlock
* 当前块
* @param fileWirter
* 写对象
* */
public CopyFile(String fileName, int threadSize, int currentBlock,
RandomAccessFile fileWirter) {
this.fileName = fileName;
this.threadSize = threadSize;
this.currentBlock = currentBlock;
this.fileWirter = fileWirter;
}
/**
* 重写的 call()
* */
@Override
public Object call() throws Exception {
// 置空
RandomAccessFile fileAccess = null;
try {
// 起始位置 currentPosition, 末尾位置 nextPosition
// 定义对源文件只读
fileAccess = new RandomAccessFile(fileName, "r");
// 获取源文件的总字节
long len = fileAccess.length();
// 根据线程的数量进行分块,每个线程负责操作自己的块内容。达到并发的效果
long step = len / threadSize;
// 根绝当前线程编号,得到起始指针指向位置
long currentPosition = step * (currentBlock - 1);
// 得到末尾指针指向位置
long nextPosition = currentPosition + step;
// 分块的时候,会出现分割的临界点在一行数据的中间,此时需要制定一个策略
// 我这里设置的是,头指针如果不是指向这一行的开头,那么使用递归向前一位查找
// 对比判断是否是换行符,是则将本行输出
// 换行符有三种:\r\n, \r, \n
// 处理"\r\n"情况
// 指向末尾,获取字符,判断是否\r\n中的\n,是的话末尾位置向下移一位,可以将本行输出
fileAccess.seek(nextPosition);
char lastChar = (char) fileAccess.read();
if (lastChar == '\n') {
nextPosition += 1;
}
// 指向起始位置
fileAccess.seek(currentPosition);
if (currentPosition > 0) {
currentPosition += 1;
long offset = 1;
// 起始位置后退一位
fileAccess.seek(currentPosition - offset);
// 获得后退一位的字符
int beforeChar = fileAccess.read();
// 递归判断是否是 \r, \n
while (beforeChar != '\n' && beforeChar != '\r') {
offset += 1;
fileAccess.seek(currentPosition - offset);
beforeChar = fileAccess.read();
}
}
// 临时变量
String lineValue = "";
long filePointer;
// 判断当行是否为空
while ((lineValue = fileAccess.readLine()) != null) {
// 获得当前指向的字节下标
filePointer = fileAccess.getFilePointer();
// 届点
if (filePointer != len && filePointer > nextPosition) {
break;
} else if (!lineValue.isEmpty()) {
// 这里是过滤的条件
if (!lineValue.endsWith("@live.com")) {
// 写
fileWirter.write((lineValue + "\n").getBytes());
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭
if (fileAccess != null) {
fileAccess.close();
}
}
return null;
}
}
时间有点赶,可能有些地方写的不是很好。还请大家多多批评指正,若觉得本文章有帮助到你,还希望可以点赞鼓励下。谢谢阅读~