多个线程同时统计请求数时写count看起来简单却不是一个不可分割的操作。它包含读取、计算和写回两个线程可能都读到同一个旧值各自加一后写回导致一次更新被覆盖。并发问题不一定每次运行都出现因此“我本地跑过没错”不能证明实现正确。先写一个可验证的计数器示例开启四个工作任务每个任务计数 25000 次主线程等所有任务完成后再读取结果。AtomicInteger提供原子的递增Future.get()既等待完成也把任务异常传回主线程。importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.CountDownLatch;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;importjava.util.concurrent.Future;importjava.util.concurrent.atomic.AtomicInteger;publicclassCounterDemo{publicstaticvoidmain(String[]args)throwsException{AtomicIntegercountnewAtomicInteger();CountDownLatchstartnewCountDownLatch(1);ExecutorServicepoolExecutors.newFixedThreadPool(4);try{ListFuture?tasksnewArrayList();for(intworker0;worker4;worker){tasks.add(pool.submit(()-{start.await();for(inti0;i25_000;i){count.incrementAndGet();}returnnull;}));}start.countDown();for(Future?task:tasks){task.get();}System.out.println(count.get());// 100000}finally{start.countDown();pool.shutdown();}}}起跑闩锁让工作任务在提交后等待同一信号它有利于构造竞争场景但不保证四个线程在同一个机器指令时刻真正并行。finally再调用一次countDown()是为了防止提交过程中出错时任务永远卡在闩锁上。shutdown()不再接收新任务但允许已提交任务继续执行示例正常路径已通过get()等待所有任务完成。原子递增不等于整个业务原子incrementAndGet()能保证单次计数更新不会丢失却不能把任意一组操作自动打包成事务。例如“如果余额足够就扣款”包含检查和修改两步单独用get()判断再用addAndGet()扣减中途仍可能被其他线程插入。可使用compareAndSet循环维护一个原子值或者使用锁、数据库事务等机制保护更大的不变量选择取决于共享状态的范围。计数器也有适用边界。用于监控的高频累计值可考虑LongAdder它通过分散竞争提高高并发更新场景的吞吐潜力但sum()在并发更新期间不是一次原子快照。需要每次读取都代表严格一致值时不要只因基准测试快就替换AtomicInteger。如果需要在应用重启后保留计数内存原子类也不是持久化方案。线程池不是“固定四条线程”这么简单Executors.newFixedThreadPool(4)方便演示但其任务队列可能持续增长。若外部流量长期大于处理能力排队会推高内存占用和请求等待时间。生产服务应结合业务并发上限、任务耗时、队列容量和拒绝策略配置线程池并为阻塞型 I/O 与计算型工作分别评估容量。盲目把线程数调大还可能压垮数据库连接池或下游接口。任务异常也不能悄悄丢掉。用submit返回的Future时异常通常在get()处被报告若提交后完全不读取结果主流程可能看起来成功实际部分任务早已失败。批处理应明确是“任一任务失败就整体失败”还是“收集每个任务的成功与失败”。相应地取消、超时和汇总策略也应在接口层写清楚。收尾、取消和中断示例的main在任务正常完成后才退出。长期运行服务则应由应用生命周期统一管理线程池停机时停止接收新任务并在给定时间内等待已有任务完成超时后如何取消或强制结束需要按数据一致性决定。shutdownNow()会尝试中断运行任务并返回未开始任务但不能保证任务立即停止尤其是阻塞在不支持中断的外部调用时。如果工作任务捕获InterruptedException不要无条件吞掉它然后继续循环。可以让异常传播或恢复线程中断标记并退出当前工作。日志中出现“停机卡住”时应同时查看线程池队列、任务中的网络超时、锁等待及中断处理不要只盯着shutdown()那一行。测试时该验证什么先断言最终计数精确等于任务数乘每个任务的迭代次数再让一个任务故意抛异常确认主线程从Future.get()得知失败。高并发测试可以提高发现错误的概率但不能靠大量重复运行证明不存在竞态。关键不变量应从所用原子操作、锁或事务的语义推导并用压力测试验证吞吐和资源上限。一个线上排队案例假设任务每次需要访问数据库平均耗时不断上升入口每秒仍持续提交更多任务。即使工作线程固定为四个尚未运行的任务也会越积越多用户实际等待时间远超过数据库查询时间。此时只看“活跃线程数稳定为四”会误判系统健康。应同时观察提交速率、完成速率、队列长度、任务等待时间以及拒绝数。队列有上限并设置明确的拒绝策略才能让系统在过载时给出可控反馈。线程池的任务边界还决定了错误边界。若四个分片分别统计数据前三个成功而第四个失败不能直接把已完成结果相加当作完整报表。可以取消其他尚未完成任务并整体失败也可以返回“部分结果”并明确缺失分片但绝不能静默当成全部成功。Future.get()只是把失败交还调用者如何处理由业务一致性要求决定。AtomicInteger也要注意数值范围。它使用int长期计数可能溢出需要更大范围可用AtomicLong或适合高竞争累计场景的LongAdder。但即使换成long也不能无限期不考虑容量。监控系统常用按时间窗口归档、重置或持久化而不是依赖一个永不清零的进程内整数。进程重启、部署多实例之后每个实例的计数器也只是局部值跨实例总量需要汇总方案。锁与原子类也不是互斥的宗教选择。一个简单计数更新很适合原子类多个字段必须共同变化时适度的锁或事务往往更容易证明正确。先写出必须保持的不变量再选择最容易验证的同步手段。微基准中的单项性能不能代替在真实业务调用链上测量端到端延迟和故障恢复。对外暴露计数时还要决定读取一致性监控指标允许短暂滞后库存扣减则不能把近似值当作决策依据。示例在所有Future.get()完成后读取因此有明确的“任务都结束”边界如果在任务运行途中打印数值只能看到某个瞬间的进度。并发正确性既是更新是否丢失也是读取者是否知道这个值代表什么时刻。总结原子类保护的是指定的原子操作线程池管理的是任务执行资源。业务不变量、异常传播、排队上限与停机行为都需要另外设计。参考Oracle Java 17 API 文档AtomicInteger、LongAdder、ExecutorService、CountDownLatch。