坚持打卡!


这个主要实现的是,多线程处理大文件,这里的大文件指的是好几十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;
	}

}

时间有点赶,可能有些地方写的不是很好。还请大家多多批评指正,若觉得本文章有帮助到你,还希望可以点赞鼓励下。谢谢阅读~