【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",符合预期。

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

相关推荐
子兮曰8 小时前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
小羊没烦恼!8 小时前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
子兮曰8 小时前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
俊昭喜喜里8 小时前
java中的继承和多态的区别
java
小羊没烦恼!8 小时前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
爱勇宝8 小时前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
譕痕8 小时前
JSONObject与JSONArray封装数据格式区别
java·json
胡写代码8 小时前
别再前后端各写一套表单校验了
java·后端
小鱼能吃糖9 小时前
缺陷修复总览 · mall电商项目:5类缺陷,1个病根,4个业务域
java·电商
此时不提桶,更待何时9 小时前
01-06-A-JVM排查实战详解
java·jvm