【Java】为什么有时候执行countDownLatch+线程池查询列表会丢失数据?

背景(工作中遇到的坑)

工作项目中,需要根据全部所属地域批量循环查询业务的列表进行展示,为了提升批量查询的速度,我采用的是countDownLatch + 线程池的方式。结果,速度是提上来了,但丢了一个地域的查询结果。经排查,是在执行线程池批量查询时,丢失了本应该查询该地域的线程task,所以导致没有查询该所属地域,从而导致了结果的偏差。

分析

使用 CountDownLatch 和线程池进行查询列表时数据丢失通常是由以下因素引起的:

1. 并发问题

当多个线程并发写入共享资源(如列表或map)时,如果没有正确的同步机制,可能会出现数据丢失或覆盖的情况。常见问题包括:

  • 没有正确的同步 :在多线程情况下,访问共享资源时没有进行适当的同步控制(如使用 synchronized 或其他并发工具类)。
  • 竞争条件:多线程同时执行,导致写操作的顺序和结果不可预测。

2. 任务未完全执行

有时候,线程池中的任务还没有完全执行完毕,主线程就已经结束了等待。这可能是由于 CountDownLatch 的计数器没有正确管理导致的。

3. 线程池配置不当

线程池的配置(如核心线程数、最大线程数、队列大小)不匹配任务的要求,导致部分任务无法执行或被丢弃:

  • 线程数限制:线程池中的线程数不足以处理所有任务。
  • 队列溢出:如果任务提交的速度超过线程池处理的速度,而且队列容量已满,新的任务可能被拒绝。

4. 异常处理不当

如果在线程中发生异常,但未被捕获和处理,任务可能会提前终止,从而导致数据的不完整。

代码示例

常见的示例代码如下:

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class CountDownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        int numberOfThreads = 10;
        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);
        CountDownLatch latch = new CountDownLatch(numberOfThreads);
        List<String> resultList = new ArrayList<>();

        for (int i = 0; i < numberOfThreads; i++) {
            int threadId = i;
            executorService.execute(() -> {
                try {
                    // 执行体
                    Thread.sleep(100);
                    resultList.add("thread-" + threadId);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();
        executorService.shutdown();
        
        System.out.println("Number of results: " + resultList.size());
    }
}

执行结果:不唯一,有时输出的resultList.size()的值为10,有时会<10,不符合预期。

解决方案和建议

  1. 使用线程安全的数据结构 :

    确保使用线程安全的数据结构来存储结果。例如,可以使用 CopyOnWriteArrayListConcurrentHashMap 代替普通的 ArrayListHashMap

    ini 复制代码
    java
    List<String> resultList = new CopyOnWriteArrayList<>();
  2. 合理配置线程池 :

    确保线程池的配置与任务量匹配。调整线程池核心线程数、最大线程数和任务队列大小。

  3. 检测并处理异常 :

    捕获并处理所有可能的异常,以防止任务意外终止。

    java 复制代码
    executorService.execute(() -> {
        try {
            // Simulate some work
            Thread.sleep(100);
            resultList.add("result");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            latch.countDown();
        }
    });
  4. 适当使用同步机制 :

    如果使用非线程安全的数据结构,如 ArrayList,需要对其进行同步。同样适用于其他共享资源。

    java 复制代码
    List<String> resultList = Collections.synchronizedList(new ArrayList<>());

代码改进

下面是改进后的示例:

java 复制代码
import java.util.List;
import java.util.concurrent.*;

public class CountDownLatchFixedTest {
    public static void main(String[] args) throws InterruptedException {
        int numberOfThreads = 10;
        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);
        CountDownLatch latch = new CountDownLatch(numberOfThreads);
        List<String> resultList = new CopyOnWriteArrayList<>();

        for (int i = 0; i < numberOfThreads; i++) {
            int threadId = i;
            executorService.execute(() -> {
                try {
                    // 执行体
                    Thread.sleep(100);
                    resultList.add("thread-" + threadId);
                } catch (Exception e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();
        executorService.shutdown();
        
        System.out.println("Number of results: " + resultList.size());
    }
}

执行结果:多次验证输出"Number of results: 10",符合预期。

通过这些改进,可以更好地保证在并发情况下数据不会丢失。

相关推荐
你的人类朋友12 分钟前
🤔Token 存储方案有哪些
前端·javascript·后端
烛阴12 分钟前
从零开始:使用Node.js和Cheerio进行轻量级网页数据提取
前端·javascript·后端
liuyang___29 分钟前
日期的数据格式转换
前端·后端·学习·node.js·node
bxlj_jcj43 分钟前
深入剖析Debezium:CDC领域的“数据魔法棒”
java·架构
叶 落1 小时前
ubuntu 安装 JDK8
java·ubuntu·jdk·安装·java8
爱学习的白杨树1 小时前
Sentinel介绍
java·开发语言
XW1 小时前
java mcp client调用 (modelcontextprotocol)
java·llm
保持学习ing2 小时前
SpringBoot前后台交互 -- 登录功能实现(拦截器+异常捕获器)
java·spring boot·后端·ssm·交互·拦截器·异常捕获器
gadiaola2 小时前
【JVM面试篇】高频八股汇总——类加载和类加载器
java·jvm·面试
七七&5562 小时前
【Java开发日记】基于 Spring Cloud 的微服务架构分析
java·spring cloud·架构