使用多线程快速向Excel中快速插入一万条数据案例

当有大量数据需要存入Excel时,使用传统的单线程完成会有以下这些弊端:

  • 导入速度慢:单线程一次只能处理一个任务,在导入大量数据时,需要逐个将数据写入 Excel。这意味着 CPU 在大部分时间里只能处理一个数据块,其他时间可能处于闲置状态,无法充分利用多核处理器的优势,导致导入过程耗时较长。
  • 界面卡顿:如果在图形界面应用程序中使用单线程进行 Excel 导入,在导入过程中,主线程会被阻塞,导致界面无法响应用户的操作,如点击按钮、滚动窗口等,给用户造成程序 "卡死" 的感觉,严重影响用户体验。
  • 资源利用率低:单线程无法同时利用 CPU 的多个核心,也不能在 I/O 操作等待时让其他任务占用 CPU,使得系统资源(如 CPU、内存等)不能得到充分利用,系统整体性能无法得到有效发挥。

下面我们一起使用多线程来优化代码,生成的Excel存将放在项目目录下(也可以自定义目录):

java 复制代码
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_COUNT
        ExecutorService 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;
        // 当前线程的 ID
        private 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;
        }

        /**
         * 线程执行的具体任务,负责在指定的行范围内插入数据。
         */
        @Override
        public void run() {
            // 记录线程开始执行任务的信息
            TASK_LOGGER.info("线程 " + threadId + " 开始执行任务,处理行范围:" + startRow + " - " + endRow);
            // 遍历当前任务需要处理的行
            for (int rowIndex = startRow; rowIndex < endRow; rowIndex++) {
                // 使用 synchronized 块确保同一时间只有一个线程可以访问 sheet
                synchronized (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 软件对多线程操作的稳定性也可能存在差异。

相关推荐
考虑考虑16 分钟前
Jpa使用union all
java·spring boot·后端
用户37215742613538 分钟前
Java 实现 Excel 与 TXT 文本高效互转
java
浮游本尊2 小时前
Java学习第22天 - 云原生与容器化
java
渣哥3 小时前
原来 Java 里线程安全集合有这么多种
java
间彧4 小时前
Spring Boot集成Spring Security完整指南
java
间彧4 小时前
Spring Secutiy基本原理及工作流程
java
Java水解5 小时前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆7 小时前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学7 小时前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole8 小时前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端