使用多线程快速向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 软件对多线程操作的稳定性也可能存在差异。

相关推荐
DXM052116 分钟前
牟乃夏《ArcGIS Engine地理信息系统开发教程》学习笔记3-地图基本操作与实战案例
开发语言·笔记·学习·arcgis·c#·ae·arcgis engine
程序员JerrySUN28 分钟前
驱动开发硬核特训 · Day 21(上篇) 抽象理解 Linux 子系统:内核工程师的视角
java·linux·驱动开发
只因只因爆1 小时前
如何在idea中写spark程序
java·spark·intellij-idea
你憨厚的老父亲突然1 小时前
从码云上拉取项目并在idea配置npm时完整步骤
java·npm·intellij-idea
全栈凯哥1 小时前
桥接模式(Bridge Pattern)详解
java·设计模式·桥接模式
PXM的算法星球1 小时前
【软件工程】面向对象编程(OOP)概念详解
java·python·软件工程
两点王爷1 小时前
springboot项目文件上传到服务器本机,返回访问地址
java·服务器·spring boot·文件上传
小吕学编程1 小时前
ES练习册
java·前端·elasticsearch
qsmyhsgcs1 小时前
Java程序员转人工智能入门学习路线图(2025版)
java·人工智能·学习·机器学习·算法工程师·人工智能入门·ai算法工程师
Humbunklung2 小时前
PySide6 GUI 学习笔记——常用类及控件使用方法(常用类矩阵QRectF)
笔记·python·学习·pyqt