对含有一亿数据的大文件进行排序,要求使用内存小于32MB

大文件排序

题目:有10个文件,每个文件有1000万行,文件内容的每一行为一个整型数字;需要,写一个程序,将所有数字排序,分为10个文件输出,如0号文件包含前1000万个数字,1号文件文件包含1千万-2千万之间的数字,依次类推。
限制:如果使用java,-Xmx需要设置为32MB;其它语言也需限制内存为32MB。 要求:正确输出 使用多线程加分编写时长:24。
小时。提供可运行的程序,以及实现说明。
  
思路:因为内存限制为32MB,将大文件分割成可以进行内部排序的的小文件,然后利用多路归并排序进行最终外部排序。

步骤:

1,产生随机数,生成10个测试文件,10个线程同时进行。
2,将大文件分割1BM的小文件,每个线程对分割而成的内容进行内部排序后,写入文件,利用自定义阻塞线程池,每次同时写入3~4个文件。
3,将所有小文件排序后,利用多路排序算法将小文件写入最终文件。

 

使用语言:JAVA

内存:eclipse 设置 -Xmx32m

代码:

1,首先是如何利用多线程,发现在32m内存下。如果线程使用过多,会增加内存消耗,所以自定义了一个阻塞的 线程池

代码如下

package com.cjj;



/**
 * 自定义阻塞型线程池 当池满时会阻塞任务提交
 * 
 * @ClassName: BlockThreadPool
 * @Description: 通过将队列的offer改为put方法(阻塞),把TreadPool改造成阻塞队列,任务超过线程数量将会阻塞
 * @author: chujiejie
 * @date: 2018-7-19 下午5:24:54
 */
public class BlockThreadPool {

	private ThreadPoolExecutor pool = null;
	
	public BlockThreadPool(int poolSize) {
		pool = new ThreadPoolExecutor(poolSize, poolSize, 0, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(poolSize),
				new CustomThreadFactory(), new CustomRejectedExecutionHandler());
	}
	
	/**
	 * 
	 * @author chujiejie
	 *
	 */
	private class CustomThreadFactory implements ThreadFactory {
		private AtomicInteger count = new AtomicInteger(0);

		@Override
		public Thread newThread(Runnable r) {
			Thread t = new Thread(r);
			String threadName = BlockThreadPool.class.getSimpleName() + count.addAndGet(1);
			t.setName(threadName);
			return t;
		}
	}
	
	/**
	 * 改造任务入队方法
	 * @author chujiejie
	 *
	 */
	private class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
		@Override
		public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
			try {
				// 核心改造,由blockingqueue的offer改成put阻塞方法
				executor.getQueue().put(r);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	/**
	 * 包装execte方法
	 * @param runnable
	 */
	public void execute(Runnable runnable) {
		this.pool.execute(runnable);
	}
	
	/**
	 * 关闭线程池
	 */
	public void shutdown() {
		this.pool.shutdown();
	}
	
	/**
	 * 测试
	 * @param args
	 */
	public static void main(String[] args) {
		BlockThreadPool pool = new BlockThreadPool(3);
		for (int i = 1; i < 100; i++) {
			System.out.println("提交第" + i + "个任务");
			pool.execute(new Runnable() {
				@Override
				public void run() {
					try {
						System.out.println(Thread.currentThread().getId() + "=====开始");
						TimeUnit.SECONDS.sleep(10);
						System.out.println(Thread.currentThread().getId() + "=====结束");
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			});
			System.out.println("提交第" + i + "个任务成功");
		}
	}


}

 

2 利用bufferread封装了一个按行读取类,调用readLine方法。

package com.cjj;

import java.io.BufferedReader;
import java.io.IOException;

/**
 * 
 * @Description: File包装类,用于按行读取
 * @author: chujiejie
 * @date: 2018-7-19 下午5:24:54
 *
 */
public class FileEntity {
	
	private int id = 0;
	private String line = "";
	private BufferedReader br;
	
	/**
	 * 传入文件 BufferedReader,包装成FileEntity
	 * 
	 * @param br
	 * @throws IOException
	 */
	public FileEntity(BufferedReader br) throws IOException {
		this.br = br;
		// 初始化读取第一行
		setLineId();
	}

	/**
	 * 使用来排序的数据
	 * 
	 * @throws IOException
	 */
	private void setLineId() throws IOException {
		line = br.readLine();
		if (line != null) {
			try {
				id = Integer.parseInt(line.trim());
			} catch (NumberFormatException e) {
				id = 0;
			}
		}
	}

	/**
	 * 关闭流
	 */
	public void close() {
		if (this.br != null) {
			try {
				this.br.close();
			} catch (Exception e) {
			}
		}
	}

	/**
	 * 读取下一行
	 * 
	 * @return FileEntity
	 */
	public FileEntity nextLine() {
		try {
			setLineId();
		} catch (IOException e) {
		}
		return this;
	}
	
	/**
	 * 
	 * @return
	 */
	public int getId() {
		return id;
	}
	
	/**
	 * 
	 * @param id
	 */
	public void setId(int id) {
		this.id = id;
	}
	
	/**
	 * 
	 * @return String
	 */
	public String getLine() {
		return line;
	}
}

3 核心代码

package com.cjj;


/**
 * 题目:有10个文件,每个文件有1000万行,文件内容的每一行为一个整型数字;需要,写一个程序,将所有数字排序,分为10个文件输出,如0号文件包含前1000万个数字,1号文件文件包含1千万-2千万之间的数字,依次类推。
 * 限制:如果使用java,-Xmx需要设置为32MB;其它语言也需限制内存为32MB。 要求:正确输出 使用多线程加分编写时长:24。
 * 小时。提供可运行的程序,以及实现说明。
 * 
 * 思路:因为内存限制为32MB,将大文件分割成可以进行内部排序的的小文件,然后利用多路归并排序进行最终外部排序。 步骤:
 * 1,产生随机数,生成10个测试文件,10个线程同时进行。
 * 2,将大文件分割1BM的小文件,每个线程对分割而成的内容进行内部排序后,写入文件,利用自定义阻塞线程池,每次同时写入3~4个文件。
 * 3,将所有小文件排序后,利用多路排序算法将小文件写入最终文件。
 * 
 * @ClassName: LargeFileSort
 * @Description: 排序十个文件,每个文件一千万个数据
 * @author chujiejie
 * @date: 2018-7-19 下午5:24:54
 * 
 */
public class LargeFileSort {

	// 存放测试和结果文件路径,改变环境修改此地址
	private final static String ROOT_FILE_PATH = "C:/Users/Administrator/Desktop/homework/files";
	// 测试文件
	private static String[] genFiles = new String[10];
	// 切分大文件的512k, 默认为 512k
	private final static int SIZE = 1024*10;
	// 切分文件大小/
	private final static int BYTE_SIZE = SIZE * 1024;
	// 线程任务完成计数器
	private static CountDownLatch doneSignal;
	// 切分文件
	private static final List<File> divFiles = new CopyOnWriteArrayList<>();

	/**
	 * 入口函数
	 * 
	 * @param args
	 * @throws Exception
	 */
	public static void main(String[] args) throws Exception {
		Long root = System.currentTimeMillis();
		// 生成测试数据
		generateTestFiles();

		Long gen = System.currentTimeMillis();
		System.out.println("\n***************");
		System.out.println(String.format("初始化数据完成:%s s。", (gen - root) / 1000));
		System.out.println("***************");
		Long divStart = System.currentTimeMillis();
		// 切分大文件成排序好的小文件,参数为同时执行线程数量
		divisionAndSortFiles(2);

		Long divEnd = System.currentTimeMillis();
		System.out.println("\n***************");
		System.out.println(String.format("切分数据完成:%s s。", (divEnd - divStart) / 1000));
		System.out.println("***************");
		Long mergeStart = System.currentTimeMillis();
		// 最终合并所有小文件
		merge();

		Long mergeEnd = System.currentTimeMillis();
		System.out.println("\n***************");
		System.out.println(String.format("合并完成:%s 秒。", (mergeEnd - mergeStart) / 1000));
		System.out.println("\n***************");
		System.out.println(
				String.format("排序完成:%s s。 %s 分钟 。", (mergeEnd - divStart) / 1000, (mergeEnd - divStart) / 1000 / 60));
		System.out.println("***************");
		System.out.println("最终文件地址:" + divFiles.get(0).getAbsolutePath());
		// 验证
		validation();
		System.exit(0);
	}

	/**
	 * 产生测试文件
	 */
	public static void generateTestFiles() {
		// 启用十个线程生成测试文件
		ExecutorService executorService = Executors.newFixedThreadPool(10);
		doneSignal = new CountDownLatch(10);
		for (int i = 0; i < genFiles.length; i++) {
			genFiles[i] = ROOT_FILE_PATH + "/originalData" + i + ".txt";
			executorService.submit(new generateFileTask(genFiles[i]));
		}
		try {
			// 等待生成文件
			doneSignal.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		executorService.shutdown();
	}

	/**
	 * 
	 * @author chujiejie
	 *
	 */
	static class generateFileTask implements Runnable {
		final String filePath;

		public generateFileTask(String filePath) {
			this.filePath = filePath;
		}

		@Override
		public void run() {
			try {
				generateTestFile(filePath);
				// 任务执行完毕递减锁存器
				doneSignal.countDown();
				System.out.print("生成文件:" + doneSignal.getCount() + ",  ");
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * 创建文件
	 * 
	 * @param filePath
	 * @throws Exception
	 */
	public static void generateTestFile(String filePath) throws Exception {
		Random random = new Random();
		StringBuffer sb = new StringBuffer();
		FileChannel channel = null;
		RandomAccessFile rf = null;
		try {
			File testFile = new File(filePath);
			if (testFile.exists())
				testFile.delete();
			// 如果文件不存在则创建
			testFile.createNewFile();

			rf = new RandomAccessFile(testFile, "rw");
			channel = rf.getChannel();
			int capacity = 1024 * 1024; // 字节
			ByteBuffer writerBuffer = ByteBuffer.allocate(capacity);
			// 一千万
			for (long i = 0; i <= 10000 * 1000; i++) {
				sb.append(random.nextInt(100000)).append("\n");
				// 刷新缓冲
				if ((i + 1) % 10000 == 0) {
					writerBuffer.put(sb.toString().getBytes());
					sb.setLength(0);
					writerBuffer.flip();
					channel.write(writerBuffer);
					writerBuffer.clear();
				}
			}
			if (sb.length() > 0) {
				writerBuffer.put(sb.toString().getBytes());
				writerBuffer.flip();
				channel.write(writerBuffer);
				writerBuffer.clear();
			}
		} catch (IOException e) {
			System.out.println("生成测试文件失败!" + e.getMessage());
			throw e;
		} finally {
			try {
				if (rf != null) {
					rf.close();
				}
			} catch (IOException e) {
				// no-op
			}
			try {
				if (channel.isOpen()) {
					channel.close();
				}
			} catch (IOException e) {
				// no-op
			}

		}
	}

	/**
	 * 分割文件task
	 * 
	 * @author chujiejie
	 *
	 */
	static class divWorkTask implements Runnable {
		final String filePath;

		public divWorkTask(String filePath) {
			this.filePath = filePath;
		}

		@Override
		public void run() {
			try {
				List<File> lists = divWork(filePath);
				// 将分割完成的文件存入
				divFiles.addAll(lists);
				// 任务执行完毕递减锁存器
				doneSignal.countDown();
				System.out.println("完成分割任务:" + doneSignal.getCount());
			} catch (Exception e) {
				System.out.println(e.getMessage());
				e.printStackTrace();
			}
		}
	}

	/**
	 * 合并分为两步,第一步将所有小文件合并和成较大的文件 , 第二部将所有文件合并最终结果。
	 * 
	 * @throws IOException
	 */
	private static void merge() throws IOException {
		List<File> divfiles2 = new ArrayList<>(divFiles);
		divFiles.clear();
		// 如果不满足条件直接合并
		if (divfiles2.size() < 20) {
			mergeLargeFile(divfiles2, ROOT_FILE_PATH + "/resultFile.txt");
			return;
		}
		List<List<File>> divTwo = new ArrayList();
		// 划分任务,15个为一组
		for (int i = 0; i < divfiles2.size(); i += 15) {
			List<File> files = new ArrayList();
			for (int j = i; j < divfiles2.size() && j < (i + 15); j++) {
				files.add(divfiles2.get(j));
			}
			divTwo.add(files);
		}
		// 定义一个三个线程的阻塞线程池
		BlockThreadPool pool = new BlockThreadPool(3);
		doneSignal = new CountDownLatch(divTwo.size());
		for (int i = 0; i < divTwo.size(); i++) {
			pool.execute(new mergeTask(divTwo.get(i), ROOT_FILE_PATH + "/temp_reslt" + i + ".txt"));
		}
		try {
			// 等待合并文件完成
			doneSignal.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		pool.shutdown();
		System.out.println("最终合并");
		// 最终合并
		File resultFile = mergeLargeFile(divFiles, ROOT_FILE_PATH + "/temp_reslt_all.txt");
		divFiles.clear();
		divFiles.add(resultFile);
	}

	/**
	 * 合并任务
	 * 
	 * @author chujiejie
	 *
	 */
	static class mergeTask implements Runnable {
		private final List<File> ListFiles;
		private final String fileName;

		public mergeTask(List<File> ListFiles, String fileName) {
			this.fileName = fileName;
			this.ListFiles = ListFiles;
		}

		@Override
		public void run() {
			try {
				File file = mergeLargeFile(ListFiles, fileName);
				divFiles.add(file);
				// 任务执行完毕递减锁存器
				doneSignal.countDown();
				System.out.println("完成合并任务:" + doneSignal.getCount());
			} catch (Exception e) {
				System.out.println(e.getMessage());
				e.printStackTrace();
			}
		}
	}

	/**
	 * 将文件分割并排序
	 */
	public static void divisionAndSortFiles(int concurrentNum) {
		// 自定义线程池
		BlockThreadPool blockPool = new BlockThreadPool(concurrentNum);
		// 切分任务开始
		doneSignal = new CountDownLatch(10);
		for (int i = 0; i < 10; i++)
			blockPool.execute(new divWorkTask(genFiles[i]));
		try {
			// 等待切分完成
			doneSignal.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		// 关闭线程池
		blockPool.shutdown();
	}

	/**
	 * 
	 * @param filePath
	 * @return
	 * @throws IOException
	 */
	public static List<File> divWork(String filePath) throws IOException {
		File file = new File(filePath);
		if (!file.exists())
			throw new Error("文件不存在");
		// 得到文件大小k
		int mbsize = (int) Math.ceil(file.length() / 1024);
		// 计算得到切分的文件数
		int fileNum = (int) Math.ceil(mbsize / SIZE) + 1;
		// 创建零时文件
		List<File> tempFileList = createTempFileList(file, fileNum);
		// 切分文件
		divAndSort(file, tempFileList);
		return tempFileList;

	}

	/**
	 * 把临时文件合并到结果文件
	 * 
	 * @param tempFileList
	 * @throws IOException
	 */
	public static File mergeLargeFile(List<File> tempFileList, String fileName) throws IOException {
		List<FileEntity> bwList = new ArrayList<FileEntity>();
		for (int i = 0; i < tempFileList.size(); i++) {
			FileEntity le = new FileEntity(
					new BufferedReader(new InputStreamReader(new FileInputStream(tempFileList.get(i)))));
			bwList.add(le);
		}
		BufferedWriter resultBw = null;
		try {
			resultBw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName)));
			int count = 0;
			FileEntity fe = null;
			StringBuilder sb = new StringBuilder();
			while ((fe = getFirstFileEntity(bwList)) != null) {
				count++;
				// 写入符合条件的一行数据
				sb.append(fe.getLine()).append("\n");
				// // 准备下一行
				fe.nextLine();
				// 清缓冲流
				if ((count + 1) % (10000) == 0) {
					resultBw.write(sb.toString());
					sb.setLength(0);
					resultBw.flush();
				}
			}
			if (sb.length() > 0) {
				resultBw.write(sb.toString());
				resultBw.flush();
				sb = null;
			}
		} catch (Exception e) {
			// no-op
		} finally {
			if (resultBw != null) {
				try {
					resultBw.flush();
					resultBw.close();
				} catch (IOException e) {
					// no-op
				}
			}
		}
		// 关闭
		for (int i = 0; i < bwList.size(); i++) {
			bwList.get(i).close();
		}
		return new File(fileName);
	}

	/**
	 * 从切分的文件中按序行读取(因为切分文件时已经做好了排序)
	 * 
	 * @param bwList
	 * @return
	 */
	private static FileEntity getFirstFileEntity(List<FileEntity> bwList) {
		if (bwList.size() == 0) {
			return null;
		}
		Iterator<FileEntity> it = bwList.iterator();
		while (it.hasNext()) {
			FileEntity fe = it.next();
			// 如果文件读到完就关闭流和删除在集合的文件流
			if (fe.getLine() == null) {
				fe.close();
				it.remove();
			}
		}
		if (bwList.size() == 0) {
			return null;
		}
		// 排序获取一行数据
		bwList.sort(new FileEntityComparator());
		// 返回第一个符合条件的文件对象
		return bwList.get(0);
	}

	/**
	 * 切分文件并做第一次排序
	 * 
	 * @param file
	 * @param tempFileList
	 */
	private static void divAndSort(File file, List<File> tempFileList) {
		BufferedReader bRead = null;
		try {
			// 读取大文件
			bRead = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
			// 行数据保存对象
			String line = null;
			// 临时文件索引
			int index = tempFileList.size() - 1;
			// 保存数据
			List<String> lineList = new ArrayList<>();
			// 统计文件大小
			int byteSum = 0;
			// 循环临时文件并循环大文件
			while ((line = bRead.readLine()) != null) {
				line += "\n";
				byteSum += line.getBytes().length;
				// 如果长度达到每个文件大小则重新计算
				if (byteSum >= BYTE_SIZE) {
					Long time0 = System.currentTimeMillis();
					// 写入到文件
					putLineListToFile(tempFileList.get(index), lineList);
					Long time1 = System.currentTimeMillis();
					System.out.println(String.format("写入文件%s:%s ms", index, time1 - time0));
					index--;
					byteSum = line.getBytes().length;
					lineList.clear();
				}
				lineList.add(line);
			}
			if (lineList.size() > 0) {
				// 写入到文件
				putLineListToFile(tempFileList.get(0), lineList);
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if (bRead != null) {
					bRead.close();
				}
			} catch (IOException e) {
				// no-op
			}
		}
	}

	/**
	 * 把数据写到临时文件
	 * 
	 * @param lineList
	 */
	private static void putLineListToFile(File file, List<String> lineList) throws IOException {
		FileOutputStream tempFileFos = null;
		try {
			// 第一次写入文件时,调用Collection.sort进行内部排序
			lineList.sort(new LineComparator());
			tempFileFos = new FileOutputStream(file);
			StringBuilder sb = new StringBuilder();
			for (int i = 0; i < lineList.size(); i++) {
				sb.append(lineList.get(i));
				if ((i + 1) % 1000 == 0) {
					tempFileFos.write(sb.toString().getBytes());
					sb.setLength(0);
				}
			}
			if (sb.length() > 0) {
				tempFileFos.write(sb.toString().getBytes());
			}
			sb = null;
		} finally {
			if (tempFileFos != null) {
				tempFileFos.close();
			}
		}
	}

	/**
	 * AIO异步写文件
	 * 
	 * @param file
	 * @param lineList
	 * @throws IOException
	 */
	private static void putLineListToFileNIO(File file, List<String> lineList) throws IOException {
		Path path = Paths.get(file.getAbsolutePath());
		AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
		ByteBuffer buffer = ByteBuffer.allocate(1024 * 512);
		Future<Integer> operation = null;
		try {
			// 第一次写入文件时,调用Collection.sort进行内部排序
			lineList.sort(new LineComparator());
			StringBuilder sb = new StringBuilder();
			for (int i = 0; i < lineList.size(); i++) {
				sb.append(lineList.get(i));
				if ((i + 1) % 1000 == 0) {
					if (operation != null) {
						while (!operation.isDone())
							;
					}
					buffer.put(sb.toString().getBytes());
					buffer.flip();
					operation = fileChannel.write(buffer, 0);
					buffer.clear();
					sb.setLength(0);
				}
			}
			if (sb.length() > 0) {
				buffer.put(sb.toString().getBytes());
				buffer.flip();
				operation = fileChannel.write(buffer, 0);
				buffer = null;
			}
			sb = null;
		} finally {
			if (operation != null) {
				while (!operation.isDone())
					;
			}
			fileChannel.force(true);
			if (fileChannel.isOpen()) {
				fileChannel.close();
			}
		}
	}

	/**
	 * 生成零时文件
	 * 
	 * @param file
	 * @param fileNum
	 * @return List<File>
	 */
	private static List<File> createTempFileList(File file, int fileNum) {
		List<File> tempFileList = new ArrayList<File>();
		String fileFolder = file.getParent();
		String name = file.getName();
		for (int i = 0; i < fileNum; i++) {
			// 创建临时文件
			File tempFile = new File(fileFolder + "/" + name + ".temp_" + i + ".txt");
			// 如果存在就删除
			if (tempFile.exists()) {
				tempFile.delete();
			}
			try {
				tempFile.createNewFile();
			} catch (IOException e) {
				e.printStackTrace();
			}
			tempFileList.add(tempFile);
		}
		return tempFileList;
	}

	/**
	 * @param o1
	 * @param o2
	 * @return
	 */
	public static int compare(String o1, String o2) {
		o1 = o1.trim();
		o2 = o2.trim();
		// 从小到大
		return Integer.parseInt(o1) - Integer.parseInt(o2);
		// 从大到小
		// return Integer.parseInt(o2) - Integer.parseInt(o1);
	}

	/**
	 * 验证
	 * 
	 * @throws IOException
	 */
	private static void validation() throws IOException {
		System.out.println("执行验证");
		BufferedReader br = new BufferedReader(
				new InputStreamReader(new FileInputStream(divFiles.get(0).getAbsolutePath())));
		String line = "";
		String pre = br.readLine();
		while ((line = br.readLine()) != null) {
			if (Integer.parseInt(line.trim()) < Integer.parseInt(pre.trim())) {
				System.out.println("验证不通过");
				System.exit(0);
			}
		}
		System.out.println("验证通过");
	}
}

/**
 * 排序
 */
class LineComparator implements Comparator<String> {
	@Override
	public int compare(String o1, String o2) {
		return LargeFileSort.compare(o1, o2);
	}
}

/**
 * 排序类
 */
class FileEntityComparator implements Comparator<FileEntity> {
	@Override
	public int compare(FileEntity o1, FileEntity o2) {
		return LargeFileSort.compare(o1.getLine(), o2.getLine());
	}
}

4运行结果,

 

 

总结,还是没有完全的用到JAVA8的AIO新特性,缓冲大小不好控制,大量运用异步的时候容易使内存超出

按比特处理的时候容易出现小错误。

谢谢

  • 10
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值