☰
多线程下的脏读:为什么你的数据“看起来没问题”,结果却全是坑?
2026/10/17 10:04:37 网站建设 项目流程

网罗开发(小红书、快手、视频号同名)

大家好,我是展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括iOS、前端、Harmony OS、Java、Python等方向。在移动端开发、鸿蒙开发、物联网、嵌入式、云原生、开源等领域有深厚造诣。

图书作者:《ESP32-C3 物联网工程开发实战》
图书作者:《SwiftUI 入门,进阶与实战》
超级个体:COC上海社区主理人
特约讲师:大学讲师,谷歌亚马逊分享嘉宾
科技博主:华为HDE/HDG

我的博客内容涵盖广泛,主要分享技术教程、Bug解决方案、开发工具使用、前沿科技资讯、产品评测与使用体验。我特别关注云服务产品评测、AI 产品对比、开发板性能测试以及技术报告,同时也会提供产品优缺点分析、横向对比,并分享技术沙龙与行业大会的参会体验。我的目标是为读者提供有深度、有实用价值的技术洞察与分析。

展菲:您的前沿技术领航员
👋 大家好,我是展菲!
📱 全网搜索“展菲”,即可纵览我在各大平台的知识足迹。
📣 公众号“Swift社区”,每周定时推送干货满满的技术长文,从新兴框架的剖析到运维实战的复盘,助您技术进阶之路畅通无阻。
💬 微信端添加好友“fzhanfei”,与我直接交流,不管是项目瓶颈的求助,还是行业趋势的探讨,随时畅所欲言。
📅 最新动态:2025 年 3 月 17 日
快来加入技术社区,一起挖掘技术的无限潜能,携手迈向数字化新征程!


文章目录

    • 前言
    • 什么是多线程下的脏读?
    • 一个最常见、也最危险的场景
      • 场景设定
    • 问题代码:共享变量无锁访问
      • Demo 1:典型脏读示例
      • 你可能会想
    • 为什么这段代码会出问题?
      • `count++` 不是原子操作
      • 可见性问题
      • 脏读在真实项目里长什么样?
    • 解决方案一:synchronized(最直接、最稳)
      • 什么时候该用?
      • Demo 2:使用 synchronized 解决脏读
      • synchronized 到底解决了什么?
      • 代价是什么?
    • 解决方案二:AtomicInteger(高并发下的常用方案)
      • Demo 3:使用 AtomicInteger
      • AtomicInteger 的核心优势
      • 它的局限
    • 解决方案三:LongAdder(超高并发统计神器)
      • Demo 4:使用 LongAdder
      • LongAdder 为什么更快?
      • 注意点
    • 实际项目中该怎么选?
    • 一个真实业务例子
      • 例子:接口 QPS 统计
    • 总结

前言

在日常开发里,只要项目稍微上点规模,多线程几乎是绕不开的。
线程池、异步任务、并发计数、实时统计……
而脏读,往往就是这些场景里最容易被忽略、却最容易埋雷的问题之一。

这篇文章我们不讲教科书式定义,直接从真实问题出发,一步一步拆解:

  • 什么是多线程下的脏读
  • 为什么你“只是读个变量”,也会出问题
  • synchronized、AtomicInteger、LongAdder 分别解决了什么问题
  • 实际项目中该怎么选

什么是多线程下的脏读?

先说一句大白话版本:

脏读 = 一个线程读到了另一个线程“还没写完 / 不稳定”的数据

在单线程世界里,这种事根本不可能发生。
但在多线程环境下,只要你共享变量 + 没有同步手段,脏读就是迟早的事。

一个最常见、也最危险的场景

场景设定

假设你在做一个统计功能:

  • 多个线程不断更新一个计数器
  • 另外一个线程负责读取当前统计值并展示

你可能会写出下面这样的代码。

问题代码:共享变量无锁访问

Demo 1:典型脏读示例

publicclassDirtyReadDemo{staticintcount=0;publicstaticvoidmain(String[]args)throwsInterruptedException{Threadwriter=newThread(()->{for(inti=0;i<100_000;i++){count++;}});Threadreader=newThread(()->{for(inti=0;i<100_000;i++){if(count<0){System.out.println("读到了异常值:"+count);}}});writer.start();reader.start();writer.join();reader.join();System.out.println("最终 count = "+count);}}

你可能会想

不就是一个int自增吗?
Java 里 int 读写不是原子的吗?

这里正好是第一个坑。

为什么这段代码会出问题?

count++不是原子操作

它实际分成了三步:

  1. 从内存读取 count
  2. +1
  3. 写回内存

多个线程同时执行时,中间状态是可见的。

可见性问题

即便你只读不写,如果:

  • 一个线程在修改
  • 一个线程在读
  • 没有任何同步手段

那么读线程看到的值,可能是旧值、半更新值,甚至完全不可预期。

脏读在真实项目里长什么样?

在实际业务中,它不会像 Demo 那么“明显”,而是表现为:

  • 偶发统计错误
  • 日志里出现不可能的状态
  • QA 环境永远复现不了,线上偶发

这也是为什么脏读特别难排查。

解决方案一:synchronized(最直接、最稳)

什么时候该用?

  • 并发量不大
  • 逻辑复杂
  • 你希望绝对正确

Demo 2:使用 synchronized 解决脏读

publicclassSynchronizedDemo{staticintcount=0;staticfinalObjectlock=newObject();publicstaticvoidmain(String[]args)throwsInterruptedException{Threadwriter=newThread(()->{for(inti=0;i<100_000;i++){synchronized(lock){count++;}}});Threadreader=newThread(()->{for(inti=0;i<100_000;i++){synchronized(lock){intvalue=count;// 这里读取到的一定是一个“完整、一致”的值}}});writer.start();reader.start();writer.join();reader.join();System.out.println("最终 count = "+count);}}

synchronized 到底解决了什么?

  • 保证同一时间只有一个线程访问共享变量
  • 保证原子性 + 可见性

代价是什么?

  • 性能开销
  • 高并发下容易成为瓶颈

解决方案二:AtomicInteger(高并发下的常用方案)

如果你的需求只是:

  • 自增
  • 计数
  • 状态标记

那 AtomicInteger 基本是第一选择。

Demo 3:使用 AtomicInteger

importjava.util.concurrent.atomic.AtomicInteger;publicclassAtomicDemo{staticAtomicIntegercount=newAtomicInteger(0);publicstaticvoidmain(String[]args)throwsInterruptedException{Threadwriter=newThread(()->{for(inti=0;i<100_000;i++){count.incrementAndGet();}});Threadreader=newThread(()->{for(inti=0;i<100_000;i++){intvalue=count.get();// 永远不会读到脏数据}});writer.start();reader.start();writer.join();reader.join();System.out.println("最终 count = "+count.get());}}

AtomicInteger 的核心优势

  • 基于 CAS(Compare-And-Swap)
  • 无锁
  • 性能远好于 synchronized

它的局限

  • 只适合简单数值操作
  • 多个变量组合逻辑不适合

解决方案三:LongAdder(超高并发统计神器)

当你面对的是:

  • 高并发写
  • 偶尔读
  • 对实时精确度要求不极端

那LongAdder会非常香。

Demo 4:使用 LongAdder

importjava.util.concurrent.atomic.LongAdder;publicclassLongAdderDemo{staticLongAddercounter=newLongAdder();publicstaticvoidmain(String[]args)throwsInterruptedException{Threadwriter=newThread(()->{for(inti=0;i<100_000;i++){counter.increment();}});Threadreader=newThread(()->{for(inti=0;i<100_000;i++){longvalue=counter.sum();}});writer.start();reader.start();writer.join();reader.join();System.out.println("最终 count = "+counter.sum());}}

LongAdder 为什么更快?

  • 内部拆分成多个 Cell
  • 不同线程更新不同 Cell
  • 读的时候再汇总

注意点

  • sum()不是强一致的瞬时值
  • 适合统计类场景,不适合金融级精度

实际项目中该怎么选?

我给你一个非常工程化的选择建议:

场景推荐方案
逻辑复杂,强一致synchronized
简单计数,高并发AtomicInteger
超高并发统计LongAdder
多变量事务synchronized / Lock

一个真实业务例子

例子:接口 QPS 统计

  • 每个请求进来都 +1
  • 定时上报监控系统
  • 不要求每一毫秒都精准

LongAdder 是最合适的选择

总结

多线程下的脏读,不是“写错代码”,而是“没意识到并发模型”。

  • 没锁 ≠ 性能高
  • 原子类 ≠ 万能
  • LongAdder ≠ 精准计数

理解每种工具解决了什么问题、牺牲了什么,比死记 API 更重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询