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

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

相关推荐
Goboy9 分钟前
分库分表后ID乱成一锅粥
后端·面试·架构
不懂英语的程序猿11 分钟前
【JEECG】JVxeTable表格拖拽排序功能
前端·后端
Goboy15 分钟前
我是如何设计出高性能群消息已读回执系统的
java·后端·架构
阳光明媚sunny30 分钟前
结构型设计模式
java·设计模式
码luffyliu32 分钟前
Java:高频面试知识分享1
java·八股文
小信丶42 分钟前
Spring Boot 简单接口角色授权检查实现
java·spring boot·后端
IT乐手43 分钟前
java 或 安卓项目中耗时统计工具类
android·java
草字1 小时前
uniapp 如果进入页面输入框自动聚焦,此时快速返回页面或者跳转到下一个页面,输入法顶上来的页面出现半屏的黑屏问题。
java·前端·uni-app
博主逸尘1 小时前
uniApp实战六:Echart图表集成
java·uni-app·php
橙子家1 小时前
Bcrypt 简介与加密和验证示例【加密知多少系列_8】
后端