如何无锁等工具实现线程异步执行同步获结果?数组与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); } } }
原因分析
非原子操作的竞态条件:
countt[0]++是复合操作,包含「读取当前值→加1→写回新值」三个步骤。多线程环境下,多个线程可能同时读取到同一个旧值,各自完成加1后写回,导致最终计数被覆盖,实际增量少于任务执行次数。而AtomicInteger.incrementAndGet()通过CAS(比较并交换)机制保证了自增操作的原子性,不会出现此类问题。线程池使用错误:测试代码中每次循环新建
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

