You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何无锁等工具实现线程异步执行同步获结果?数组与AtomicInteger计数差异

线程池计数不一致问题分析与解决

问题描述

使用大小为5的ExecutorService线程池,初始化了final int[] count = {0}和AtomicInteger at = new AtomicInteger(0);循环执行1000次(测试代码中为1000次),每次任务内执行count[0]++和at.incrementAndGet(),最终两者数值不一致。

需求:不想使用锁、同步块、长延迟awaitTermination、CountDownLatch,求其他方法让两者结果一致。

测试代码

class AsyncExecutorApplicationTests {
    private static AtomicInteger count = new AtomicInteger(0);

    @Test
    void check() {
        try {
            final int[] countt = {0};
//          Lock lock = new ReentrantLock();
            for (int i = 0; i < 1000; i++) {
                ExecutorService executorService = Executors.newFixedThreadPool(5);
                executorService.execute(() -> {
                    try
                    {
                        Thread.sleep(100);
                        countt[0]++;
                        count.incrementAndGet();
                        System.out.println(Thread.activeCount()+"-----" + Thread.currentThread().getName() + "----" + count.get() + "-----" + countt[0]);
                        Thread.sleep(100);
                    }
                    catch (InterruptedException e)
                    {
                        throw new RuntimeException(e);
                    }
                }
                );
                executorService.awaitTermination(10, TimeUnit.MILLISECONDS); // blocks/waits for certain interval as specified
                executorService.shutdown();
            }
            Assertions.assertEquals(1000, count.get());
            Assertions.assertEquals(1000, countt[0]);
        }
        catch (NullPointerException | InterruptedException e)
        {
            throw new RuntimeException(e);
        }
    }
}

原因分析

  1. 非原子操作的竞态条件:countt[0]++是复合操作,包含「读取当前值→加1→写回新值」三个步骤。多线程环境下,多个线程可能同时读取到同一个旧值,各自完成加1后写回,导致最终计数被覆盖,实际增量少于任务执行次数。而AtomicInteger.incrementAndGet()通过CAS(比较并交换)机制保证了自增操作的原子性,不会出现此类问题。

  2. 线程池使用错误:测试代码中每次循环新建ExecutorService,且仅等待10ms就调用shutdown()。但任务内部需要先睡眠100ms才执行计数逻辑,10ms的等待时间远不足以让任务完成,大部分任务会在未执行计数时就被线程池关闭,进一步导致countt和count的计数都达不到预期,且两者的误差因竞态条件被放大。

替代解决方案

1. 复用线程池+原子类替换int数组

核心思路:复用一个线程池避免资源浪费,用原子类替换普通int数组保证计数原子性,通过invokeAll批量提交任务并自动等待所有任务完成,无需依赖长延迟或CountDownLatch。

修改后代码示例:

class AsyncExecutorApplicationTests {
    private static AtomicInteger count = new AtomicInteger(0);

    @Test
    void check() {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        try {
            AtomicInteger countt = new AtomicInteger(0); // 替换int[]为原子类
            List<Callable<Void>> tasks = new ArrayList<>(1000);
            
            for (int i = 0; i < 1000; i++) {
                tasks.add(() -> {
                    try {
                        Thread.sleep(100);
                        countt.incrementAndGet();
                        count.incrementAndGet();
                        System.out.println(Thread.activeCount() + "-----" + Thread.currentThread().getName() + "----" + count.get() + "-----" + countt.get());
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    return null;
                });
            }
            
            executorService.invokeAll(tasks); // 批量提交任务,等待所有任务完成
            executorService.shutdown();
            
            Assertions.assertEquals(1000, count.get());
            Assertions.assertEquals(1000, countt.get());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            if (!executorService.isTerminated()) {
                executorService.shutdownNow();
            }
        }
    }
}

2. 高并发场景用LongAdder替代AtomicInteger

如果是更高并发的场景,LongAdder的性能优于AtomicInteger,同样通过原子性保证计数准确:

// 初始化
LongAdder countt = new LongAdder();
// 自增
countt.increment();
// 获取最终计数
Assertions.assertEquals(1000, countt.intValue());

3. 修正线程池等待逻辑(不依赖长延迟)

如果一定要保留int数组结构,必须通过原子操作包装countt[0]++,但不用锁的话只能借助原子类的CAS逻辑手动实现,不如直接替换为原子类简洁。核心还是要保证线程池等待所有任务完成,比如提交所有任务后调用shutdown()再循环等待线程池终止:

executorService.shutdown();
while (!executorService.awaitTermination(1, TimeUnit.SECONDS)) {
    // 循环等待直到所有任务完成
}

内容的提问来源于stack exchange,提问作者aditya sharma

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 14:25:00