JAVA 按行处理大文件的方法 [多线程]
前一篇文章讲述了单线程处理大文件的方法,虽然解决了内存装不下的问题但是依然存在效率不高的问题。这篇文章介绍的是Java中多线程处理大文件的一种方法,如有疑问欢迎各位大神垂询,我们相互帮助、共同学习。
如果想看单线程处理大文件的那边文章请移步:Java 按行处理大文件的方法 [单线程]
一、问题的提出
按照单线程解决Java处理大文件的思路将文件分片读取,这样解决的方法效率不高。如下例:
- 代码
private static void handleInternal(List<String> lines, int index) {
File file = new File("C:\\Users\\Mr-Leaf\\Desktop\\test_new.csv") ;
StringBuilder sb = new StringBuilder() ;
for(String line : lines) {
sb.append(line + "\n") ;
}
//直接写成新文件
write(file, sb.toString()) ;
}
/**
* <p>功能: 写入文件</p>
* @param string
*/
protected static void write(File file, String str) {
try {
FileUtils.writeStringToFile(file, str , "utf-8", true);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* <p>功能: 分片读取文件</p>
* @param file 要读取的文件
* @param encoding 编码格式
* @param startLine 开始读取的行数
* @param size 要读取的量
* @return
* @throws IOException
*/
public static List<String> copyToLines(File file, String encoding, int startLine,
int size) throws IOException
{
List<String> lines = new ArrayList<String>();
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), encoding));
try
{
String line = null;
int lineNum = 0;
if (startLine < 1) startLine = 1;
while ((line = reader.readLine()) != null)
{
lineNum++;
if (startLine > 0 && lineNum < startLine) continue;
if (size > 0 && (lineNum - startLine) >= size) break;
lines.add(line);
}
}
finally
{
if (reader != null) reader.close();
}
return lines;
}
public static void handle(File file) throws IOException {
int index = 1 ;
List<String> lines = copyToLines(file, "utf-8", index, 20000) ;
while(!CollectionTools.isEmpty(lines)) {
handleInternal(lines, index) ;
System.out.println("正在处理" + index + "-" + (index+20000-1) + "处数据...");
index = index + 20000 ;
lines.clear();
lines = FileCopyTools.copyToLines(file, "utf-8", true, index, 20000) ;
}
}
public static void main(String[] args) throws IOException {
File file = new File("C:\\Users\\Mr-Leaf\\Desktop\\test.csv") ;
long start = System.currentTimeMillis() ;
handle(file) ;
System.out.println("总耗时:" + (System.currentTimeMillis()-start) + "毫秒");
}
- 输出结果
正在处理1-20000处数据...
正在处理20001-40000处数据...
正在处理40001-60000处数据...
正在处理60001-80000处数据...
正在处理80001-100000处数据...
正在处理100001-120000处数据...
正在处理120001-140000处数据...
正在处理140001-160000处数据...
正在处理160001-180000处数据...
正在处理180001-200000处数据...
正在处理200001-220000处数据...
正在处理220001-240000处数据...
正在处理240001-260000处数据...
正在处理260001-280000处数据...
正在处理280001-300000处数据...
正在处理300001-320000处数据...
正在处理320001-340000处数据...
正在处理340001-360000处数据...
总耗时:8513毫秒
二、需要解决的问题
由上边的例子可以看到,我处理的文件有153 MB,共约3500000行,耗时近9秒。如何提高效率呢?需要解决的问题:
- 如何保证线程不爆炸增长
- 如何实现线程同步
- 如何实现多个线程的任务调度
三、解决方案
- 针对控制线程数量的问题我想大家已经想到了,没错就是使用线程池管理线程,这里我使用定长池FixedThreadPool
- 由于线程同步同时也保证了多个线程读写同一个文件的任务调度
- 针对线程同步的问题,我采取的是读写分离的策略,采用生产者-消费者设计模式,如下图:
四、实现步骤
1. 定义队列和线程池
- 代码
//读取当前CPU个数,决定线程池的大小
private final static int POOLSIZE = Runtime.getRuntime().availableProcessors() ;
//线程池
private ExecutorService executor = Executors.newFixedThreadPool(POOLSIZE) ;
//任务队列
private ArrayBlockingQueue<List<String>> taskQueue = new ArrayBlockingQueue<>(POOLSIZE) ;
//判断是否需要结束
private AtomicBoolean isFinsh = new AtomicBoolean(false) ;
2. 生产者任务
- 代码
/**
* <p>功能: 往任务队列里放入任务</p>
* @param file
* @throws IOException
* @throws InterruptedException
*/
private void putTasks(File file) throws IOException, InterruptedException {
List<String> lines = new ArrayList<String>();
//这里不需要像原来一样,直接顺序分片即可,用来减少寻道时间
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), ENCODING));
try
{
String line = null;
while ((line = reader.readLine()) != null)
{
//忽略空行
if(!StringTools.hasText(line)) continue;
lines.add(line) ;
//如果list大于pageSize放入任务队列
if(lines.size() >= pageSize) {
taskQueue.put(lines);
lines = new ArrayList<String>();
}
}
//放入最后剩余部分
taskQueue.put(lines);
}
finally
{
System.out.println("==生产线程结束了==");
isFinsh.set(true);
if (reader != null) reader.close();
}
}
3. 消费者任务
- 代码
/**
* <p>功能: 多线程处理具体任务</p>
*/
private void handleSyncInternal() {
try {
AtomicInteger startIndex = new AtomicInteger(1) ;
while(!isFinsh.get()) {
List<String> lines = taskQueue.poll(3, TimeUnit.SECONDS) ;
if(CollectionTools.isEmpty(lines))
continue ;
executor.execute(() ->{
int index = startIndex.getAndAdd(pageSize) ;
System.out.println("正在处理" + index + "-" + (index+pageSize-1) + "处数据..." );
//这个方法是具体处理业务的方法,一般由子类来实现
handleInternal(lines, index) ;
});
}
} catch (Exception e) {
e.printStackTrace();
}
}
4. 主流程(任务调度)
- 代码
/**
* <p>功能: 把文件拆分成size大小分别处理,多线程 </p>
* @param file
* @param size
* @throws IOException
*/
public void handleSync(File file) throws IOException {
//将大文件拆分后放到任务队列
executor.execute(() ->{
try {
putTasks(file) ;
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}) ;
//处理业务
handleSyncInternal() ;
executor.shutdown();
while(!executor.isTerminated()) {
}
System.out.println("==任务执行完毕==");
}
5. 任务结束条件
这里的任务执行结束的条件我个人感觉我处理的不是很好,有两个条件表明任务结束:
- 生产线程读取文件结束
- 主线程三秒内从线程池里拿不到新数据
一定还有更好的判断方法,希望各位不吝赐教。
五、源代码
/**
* @desc 处理大文件的工具类
* @author cmc
* @email chenmingchuan@all-in-data.com
* @date 2019年11月25日 下午2:30:21
*/
public abstract class AbstractBigFileTools {
private static final String ENCODING = "utf-8" ;
//处理文件段大小,默认20000
private int pageSize = 20000 ;
//读取当前CPU个数,决定线程池的大小
private final static int POOLSIZE = Runtime.getRuntime().availableProcessors() ;
//线程池
private ExecutorService executor = Executors.newFixedThreadPool(POOLSIZE) ;
//任务队列
private ArrayBlockingQueue<List<String>> taskQueue = new ArrayBlockingQueue<>(POOLSIZE) ;
private AtomicBoolean isFinsh = new AtomicBoolean(false) ;
public AbstractBigFileTools(int pageSize) {
super();
this.pageSize = pageSize;
}
public AbstractBigFileTools() {
super();
}
/**
* <p>功能: 把文件拆分成size大小分别处理,单线程 </p>
* @param file
* @param size
* @throws IOException
*/
public void handle(File file) throws IOException {
int index = 1 ;
long start = System.currentTimeMillis() ;
List<String> lines = FileCopyTools.copyToLines(file, ENCODING, true, index, pageSize) ;
while(!CollectionTools.isEmpty(lines)) {
handleInternal(lines, index) ;
System.out.println("正在处理" + index + "-" + (index+pageSize-1) + "处数据,文件读取耗时:"
+ (System.currentTimeMillis()-start));
index = index + pageSize ;
lines.clear();
start = System.currentTimeMillis() ;
lines = FileCopyTools.copyToLines(file, ENCODING, true, index, pageSize) ;
}
}
/**
* <p>功能: 把文件拆分成size大小分别处理,多线程 </p>
* @param file
* @param size
* @throws IOException
*/
public void handleSync(File file) throws IOException {
// //打印日志
// executor.execute(() ->{
// printLogs() ;
// });
//将大文件拆分后放到任务队列
executor.execute(() ->{
try {
putTasks(file) ;
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}) ;
//处理业务
handleSyncInternal() ;
executor.shutdown();
while(!executor.isTerminated()) {
}
System.out.println("==任务执行完毕==");
}
// /**
// * <p>功能: 打印日志</p>
// */
// private void printLogs() {
// while(!isTerminated()) {
// try {
// TimeUnit.SECONDS.sleep(3);
// System.out.println("任务执行状态:" + executor);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// }
/**
* <p>功能: 多线程处理具体任务</p>
*/
private void handleSyncInternal() {
try {
AtomicInteger startIndex = new AtomicInteger(1) ;
while(!isFinsh.get()) {
List<String> lines = taskQueue.poll(3, TimeUnit.SECONDS) ;
if(CollectionTools.isEmpty(lines))
continue ;
executor.execute(() ->{
int index = startIndex.getAndAdd(pageSize) ;
System.out.println("正在处理" + index + "-" + (index+pageSize-1) + "处数据..." );
handleInternal(lines, index) ;
});
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* <p>功能: 往任务队列里放入任务</p>
* @param file
* @throws IOException
* @throws InterruptedException
*/
private void putTasks(File file) throws IOException, InterruptedException {
List<String> lines = new ArrayList<String>();
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), ENCODING));
try
{
String line = null;
while ((line = reader.readLine()) != null)
{
//忽略空行
if(!StringTools.hasText(line)) continue;
lines.add(line) ;
//如果list大于pageSize放入任务队列
if(lines.size() >= pageSize) {
taskQueue.put(lines);
lines = new ArrayList<String>();
}
}
//放入最后剩余部分
taskQueue.put(lines);
}
finally
{
System.out.println("==生产线程结束了==");
isFinsh.set(true);
if (reader != null) reader.close();
}
}
/**
* <p>功能: 具体业务,有子类实现</p>
* @param lines
*/
protected abstract void handleInternal(List<String> lines, int index) ;
/**
* <p>功能: 批量写入文件</p>
* @param lines
* @param file
* @param size 每次写入的行数
*/
protected void batchWrite(List<String> lines, File file, int size) {
if(CollectionTools.isEmpty(lines)) {
return ;
}
int lineSize = 0 ;
StringBuilder sb = new StringBuilder() ;
for(String line : lines) {
if(lineSize >= size) {
write(file, sb.toString()) ;
sb = new StringBuilder() ;
lineSize = 0 ;
}
sb.append(line + "\n") ;
lineSize ++ ;
}
write(file, sb.toString()) ;
}
/**
* <p>功能: 写入文件</p>
* @param string
*/
protected void write(File file, String str) {
try {
FileUtils.writeStringToFile(file, str , "utf-8", true);
} catch (IOException e) {
e.printStackTrace();
}
}
}
六、执行效果
同样,还是上边的那个文件(153 MB,共约3500000行)执行效果如下:
正在处理1-20000处数据...
正在处理20001-40000处数据...
正在处理40001-60000处数据...
正在处理60001-80000处数据...
正在处理80001-100000处数据...
正在处理100001-120000处数据...
正在处理120001-140000处数据...
正在处理140001-160000处数据...
正在处理160001-180000处数据...
正在处理180001-200000处数据...
正在处理200001-220000处数据...
正在处理220001-240000处数据...
正在处理240001-260000处数据...
正在处理260001-280000处数据...
正在处理280001-300000处数据...
正在处理300001-320000处数据...
正在处理320001-340000处数据...
==生产线程结束了==
正在处理340001-360000处数据...
==任务执行完毕==
总耗时:6417毫秒
减去判定执行完成后主线程等待的三秒,总执行时间为3417,效率提高了一倍!
再是一个5Gb大小的文件执行效果如下:
正在处理1-20000处数据...
正在处理20001-40000处数据...
正在处理40001-60000处数据...
正在处理60001-80000处数据...
正在处理80001-100000处数据...
正在处理100001-120000处数据...
正在处理120001-140000处数据...
(假装没看到的省略...)
正在处理9860001-9880000处数据...
正在处理9880001-9900000处数据...
正在处理9900001-9920000处数据...
正在处理9920001-9940000处数据...
正在处理9940001-9960000处数据...
正在处理9960001-9980000处数据...
正在处理9980001-10000000处数据...
==生产线程结束了==
正在处理10000001-10020000处数据...
==任务执行完毕==
总耗时:127632毫秒
总耗时两分钟,看一眼磁盘占用:
个人感觉已经很快了,哈哈哈。
总结
虽然问题解决了,但是还存在问题,例如判断任务执行结束的条件不准确。希望看到这篇文章的大神们能够多多指出我的错误,多多提出宝贵意见。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人