使用多线程快速向Excel中快速插入一万条数据案例
当有大量数据需要存入Excel时,使用传统的单线程完成会有以下这些弊端:
- 导入速度慢:单线程一次只能处理一个任务,在导入大量数据时,需要逐个将数据写入 Excel。这意味着 CPU 在大部分时间里只能处理一个数据块,其他时间可能处于闲置状态,无法充分利用多核处理器的优势,导致导入过程耗时较长。
- 界面卡顿:如果在图形界面应用程序中使用单线程进行 Excel 导入,在导入过程中,主线程会被阻塞,导致界面无法响应用户的操作,如点击按钮、滚动窗口等,给用户造成程序 “卡死” 的感觉,严重影响用户体验。
- 资源利用率低:单线程无法同时利用 CPU 的多个核心,也不能在 I/O 操作等待时让其他任务占用 CPU,使得系统资源(如 CPU、内存等)不能得到充分利用,系统整体性能无法得到有效发挥。
下面我们一起使用多线程来优化代码,生成的Excel存将放在项目目录下(也可以自定义目录):
package com.example.demo.demo_insert_excel;import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;/*** 该类用于演示如何使用多线程向 Excel 文件中插入 1 万条数据。* 它通过将任务分配给多个线程来提高数据插入的效率,并使用日志记录执行过程。*/
public class MultiThreadExcelInsert {// 定义要插入到 Excel 中的总行数private static final int ROW_COUNT = 10000;// 定义使用的线程数量private static final int THREAD_COUNT = 4;// 定义生成的 Excel 文件的名称private static final String FILE_NAME = "output.xlsx";// 获取当前类的日志记录器,用于记录程序执行过程中的信息private static final Logger LOGGER = Logger.getLogger(MultiThreadExcelInsert.class.getName());/*** 程序的入口点,负责初始化工作簿、分配任务给线程池,并处理文件写入和资源关闭。** @param args 命令行参数(在本程序中未使用)*/public static void main(String[] args) {// 记录程序开始执行的时间,用于后续计算总耗时long startTime = System.currentTimeMillis();// 创建一个新的 XSSF 工作簿(即 .xlsx 格式的 Excel 文件)Workbook workbook = new XSSFWorkbook();// 在工作簿中创建一个名为 "Data" 的工作表Sheet sheet = workbook.createSheet("Data");// 创建一个固定大小的线程池,线程数量为 THREAD_COUNTExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);// 计算每个线程需要处理的行数int batchSize = ROW_COUNT / THREAD_COUNT;// 用于存储每个任务的 Future 对象,以便后续等待任务完成Future<?>[] futures = new Future[THREAD_COUNT];// 为每个线程分配任务for (int i = 0; i < THREAD_COUNT; i++) {// 计算当前线程处理的起始行int startRow = i * batchSize;// 计算当前线程处理的结束行int endRow = (i == THREAD_COUNT - 1) ? ROW_COUNT : (i + 1) * batchSize;// 创建一个插入任务实例InsertTask task = new InsertTask(sheet, startRow, endRow, i);// 提交任务到线程池,并获取该任务的 Future 对象futures[i] = executor.submit(task);// 记录线程任务提交信息LOGGER.info("线程 " + i + " 已提交任务,处理行范围:" + startRow + " - " + endRow);}// 等待所有任务完成for (Future<?> future : futures) {try {// 阻塞当前线程,直到任务完成future.get();} catch (InterruptedException | ExecutionException e) {// 若任务执行过程中出现异常,中断当前线程并记录错误信息Thread.currentThread().interrupt();LOGGER.log(Level.SEVERE, "任务执行出错", e);}}// 关闭线程池,不再接受新的任务executor.shutdown();// 使用 try-with-resources 语句确保文件输出流在使用后正确关闭try (FileOutputStream fileOut = new FileOutputStream(FILE_NAME)) {// 将工作簿的内容写入到指定的文件中workbook.write(fileOut);} catch (IOException e) {// 若写入文件过程中出现异常,记录错误信息LOGGER.log(Level.SEVERE, "写入文件出错", e);} finally {try {// 关闭工作簿workbook.close();} catch (IOException e) {// 若关闭工作簿时出现异常,记录错误信息LOGGER.log(Level.SEVERE, "关闭工作簿出错", e);}}// 记录程序结束执行的时间long endTime = System.currentTimeMillis();// 记录数据插入成功的信息LOGGER.info("数据已成功插入到 " + FILE_NAME);// 记录数据插入的总耗时LOGGER.info("插入数据总共耗时: " + (endTime - startTime) + " 毫秒");}/*** 插入任务类,实现了 Runnable 接口,用于在单独的线程中执行数据插入操作。*/static class InsertTask implements Runnable {// 要操作的工作表private final Sheet sheet;// 当前任务处理的起始行private final int startRow;// 当前任务处理的结束行private final int endRow;// 当前线程的 IDprivate final int threadId;// 获取当前类的日志记录器,用于记录任务执行过程中的信息private static final Logger TASK_LOGGER = Logger.getLogger(InsertTask.class.getName());/*** 构造函数,用于初始化插入任务的相关信息。** @param sheet 要操作的工作表* @param startRow 任务处理的起始行* @param endRow 任务处理的结束行* @param threadId 当前线程的 ID*/public InsertTask(Sheet sheet, int startRow, int endRow, int threadId) {this.sheet = sheet;this.startRow = startRow;this.endRow = endRow;this.threadId = threadId;}/*** 线程执行的具体任务,负责在指定的行范围内插入数据。*/@Overridepublic void run() {// 记录线程开始执行任务的信息TASK_LOGGER.info("线程 " + threadId + " 开始执行任务,处理行范围:" + startRow + " - " + endRow);// 遍历当前任务需要处理的行for (int rowIndex = startRow; rowIndex < endRow; rowIndex++) {// 使用 synchronized 块确保同一时间只有一个线程可以访问 sheetsynchronized (sheet) {// 在工作表中创建一行Row row = sheet.createRow(rowIndex);// 遍历每一行的列for (int colIndex = 0; colIndex < 4; colIndex++) {// 在当前行中创建一个单元格Cell cell = row.createCell(colIndex);// 为单元格设置值,格式为 "数据" + 行索引 + "_" + 列索引cell.setCellValue("数据" + rowIndex + "_" + colIndex);}}// 每处理 100 行记录一次当前处理进度if (rowIndex % 100 == 0) {TASK_LOGGER.info("线程 " + threadId + " 已处理到行:" + rowIndex);}}// 记录线程完成任务的信息TASK_LOGGER.info("线程 " + threadId + " 完成任务,处理行范围:" + startRow + " - " + endRow);}}
}
最终结果:
优点
1. 提高导入速度
在多核处理器的环境下,多线程能够充分利用 CPU 的多个核心,让多个线程同时进行数据处理和写入操作。相比于单线程按顺序依次写入数据,多线程可以并行处理多个数据块,显著减少了整体的导入时间。例如,在导入大量数据时,单线程可能需要几分钟甚至更长时间,而多线程可以将时间缩短至几十秒。
2. 提升系统资源利用率
多线程可以让 CPU、内存等系统资源得到更充分的利用。当一个线程在进行 I/O 操作(如将数据写入 Excel 文件)时,CPU 可能处于空闲状态,此时其他线程可以继续执行数据处理任务,避免了资源的闲置浪费,提高了系统的整体性能。
3. 增强用户体验
对于需要处理大量数据导入的应用程序,使用多线程可以避免主线程被长时间阻塞,从而保证界面的响应性。用户在导入数据的过程中仍然可以进行其他操作,不会感觉到程序卡顿,提升了用户体验。
缺点
1. 线程安全问题
在多线程环境下,多个线程可能会同时访问和修改共享资源(如 Excel 文件的工作表、单元格等),这就容易引发线程安全问题。例如,两个线程同时尝试在同一单元格写入数据,可能会导致数据覆盖或文件损坏。为了保证线程安全,需要使用同步机制(如
synchronized
关键字、Lock
接口等),但这会增加代码的复杂度和性能开销。2. 调试和维护困难
多线程程序的执行流程比单线程程序更加复杂,线程之间的交互和调度难以预测。当出现问题时,调试和定位问题会变得非常困难,因为错误可能是由于线程之间的竞争条件、死锁等原因引起的,这些问题很难通过简单的日志和调试工具来排查。此外,多线程代码的维护也更加困难,因为需要考虑线程安全和并发控制等因素。
3. 资源竞争和性能开销
虽然多线程可以提高系统资源的利用率,但如果线程数量过多,会导致资源竞争加剧。例如,多个线程同时访问磁盘、内存等资源,可能会导致资源瓶颈,反而降低了系统的性能。此外,线程的创建、销毁和切换都需要消耗一定的系统资源,过多的线程会增加系统的负担,导致性能下降。
4. 兼容性问题
不同的 Excel 操作库对多线程的支持程度可能不同,某些库可能没有很好地处理多线程环境下的并发问题,导致在使用多线程进行数据导入时出现兼容性问题。此外,不同版本的 Excel 软件对多线程操作的稳定性也可能存在差异。