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秒。如何提高效率呢?需要解决的问题:

  1. 如何保证线程不爆炸增长
  2. 如何实现线程同步
  3. 如何实现多个线程的任务调度

三、解决方案

  1. 针对控制线程数量的问题我想大家已经想到了,没错就是使用线程池管理线程,这里我使用定长池FixedThreadPool
  2. 由于线程同步同时也保证了多个线程读写同一个文件的任务调度
  3. 针对线程同步的问题,我采取的是读写分离的策略,采用生产者-消费者设计模式,如下图:
Syntax error in textmermaid version 11.4.1

四、实现步骤

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. 任务结束条件

这里的任务执行结束的条件我个人感觉我处理的不是很好,有两个条件表明任务结束:

  1. 生产线程读取文件结束
  2. 主线程三秒内从线程池里拿不到新数据

一定还有更好的判断方法,希望各位不吝赐教。

五、源代码

/**
 * @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毫秒

总耗时两分钟,看一眼磁盘占用:
在这里插入图片描述
个人感觉已经很快了,哈哈哈。

总结

虽然问题解决了,但是还存在问题,例如判断任务执行结束的条件不准确。希望看到这篇文章的大神们能够多多指出我的错误,多多提出宝贵意见。

posted @   binbinx  阅读(285)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
点击右上角即可分享
微信分享提示