news 2026/5/1 7:49:43

菜鸟要知道的「线程安全」

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
菜鸟要知道的「线程安全」

菜鸟要知道的「线程安全」

🤫本文基于go技术栈进行解释相关概念及部分源码展示~

✨线程安全是什么东西

线程安全,就是在多个线程并发操作同个资源的时候,产生的数据始终一致

下面这个就是就是一个线程安全问题的例子:

var count int var wg sync.WaitGroup func main() { wg.Add(2) go func() { defer wg.Done(); for i := 0; i < 1000; i++ { count++ } }() go func() { defer wg.Done(); for i := 0; i < 1000; i++ { count++ } }() wg.Wait() fmt.Println(count) // 期望 2000,实际可能是 1800、1950 等任意值 } //count++ 不是原子操作(读 → 加1 → 写),多个 goroutine 交错执行会导致丢失更新。

解决办法有很多,最常见的就是加锁,同一时间只允许有一个线程可以对资源进行操作。
详细的代码在此不再作再多展示,因为我们本次的目的是研究:为什么加锁可以保证线程安全?

✨为什么加锁可以保证线程安全?

🧐吾日三省吾身

  • ⚠️是什么决定了多线程会出现不安全的情况?
  • ⚠️加锁实际上是如何保证线程安全?
  • ⚠️加锁转成底层指令会是怎么样?

🧐我们把问题拆解,逐步分析,再重新思考

🎯是什么决定了多线程会出现不安全的情况?
📔对资源的并发操作

🌰比如:一个简单修改变量的操作,转化成底层的汇编指令时,会生成多个指令。

汇编指令:读取-计算-写入 0x0000 MOVQ "".counter(SB), AX ; 读取当前值 0x0007 LEAQ 1(AX), CX ; 计算新值 (AX + 1) 0x000b MOVQ CX, "".counter(SB) ; 写回新值

如果不加锁的情况下,就会出现以下情况:

协程1协程2协程3
读取a=1读取a=1读取a=1
a++
写入a=2a++
a++
写入a=2写入a=2

解决:语言层面加入锁去确保多线程时同一时间只允许其中一个线程对资源进行

📔内存可见性

这里篇幅稍微涉及到的知识可能会稍微有点多,请耐心阅读!

在多核CPU的架构下,进程内的不同线程可能会被运行在不同的CPU核心中。
N个核心就代表这个CPU可以同时运行N个线程。

🖥️ 现代计算机的存储层次结构(从快到慢)

CPU 寄存器 ↓ L1 缓存(每个核心私有,~1ns) ↓ L2 缓存(每个核心私有或共享,~3ns) ↓ L3 缓存(所有核心共享,~10ns) ↓ 主存 RAM(~100ns) ↓ 磁盘 / SSD

看到这里就可以想到一个糟糕的事情:

线程A修改了变量a,但是一般情况下为了加快执行效率
CPU不会每次把数据写到L3或RAM中,而是会先写入L2或L1缓存中,然后发送失效广播给其他核心。(不保证马上处理)
所以运行在其他核心的线程读取变量a时,可能读到的是旧值,或者新值。

这会导致一个问题,只有当前线程或执行在该核心的线程能保证百分百读到更新后的值。

📔内存屏障

这里引入了一个「内存屏障」的概念,去解决内存可见性问题



内存屏障是“同步指令”,作用类似于操作数据库开始事务的命令,执行了内存屏障指令后,后续的指令会被内存屏障指令影响,功能可以分为三点:

  • 告诉 CPU:“把你缓存里的脏数据刷到主存!”
  • 告诉其他 CPU:“我刚刚更新了数据A,请把你们的数据A标记为过期!”
  • 告诉编译器/CPU:“别重排我屏障前后的代码!”

让更新了数据的CPU的脏数据刷到主存!

这里就是刚刚说到的,CPU操作数据时,不会直接把数据更新回主存,而是直接操作CPU的三级缓存,因为这样效率更高。
最后如果缓存数据满了,会采用淘汰算法,把淘汰的脏数据刷回主存中。

这里大家可能会有一个疑问:

❓为什么不直接更新到L3缓存就可以了?反正L3是所有核心的共享缓存。

现代的服务器一般都会有多个CPU插槽,每个CPU之间的L3是不可以互相访问的,所以要把更改的数据刷回主存才能让所有的CPU能找到最新的数据。

我刚刚更新了数据A,请把你们的数据A标记为过期!

当核心0修改了数据后并启用了内存屏障命令

mov [x], 1 ; 写入 x = 1(先写入 Core0 的 store buffer / cache) mfence ; 内存屏障

缓存一致性协议(如 MESI)被触发

  • Core 0 的缓存行状态变为Modified (M)
  • 如果其他核心(如 Core 1)的缓存中有x的副本(状态为 Shared 或 Invalid),
    Core 0 会广播 “Invalidate” 消息

Core 1 收到 Invalidate 后

  • 将自己缓存中x的副本标记为Invalid (I)
  • 下次读x时,发现缓存行无效 → 触发 cache miss → 从其他核心的缓存或主存加载最新值(优先从其他核心读取)

别重排我屏障前后的代码!

这里涉及到的是「指令重排」的问题
因为一般情况下,编译器会自己优化编排命令的执行顺序。

a := true b := "" go func() { b = "msg" a = false }() for a { } println(b) //打印结果有可能为空 //原因:编译器对指令进行了优化重新编排b="msg"被安排在a=false之后

这里可以理解为:
晚上去大排档吃夜宵,你点了一份炒面,另外有两个客人点了两份炒粉。老板可能会优先把两份炒粉一起炒了先,再安排炒面。
但是老板也有可能是个守规矩的人,先把你的炒面炒了,再给其他两个客人炒粉。
所以最终的结果取决于老板当时的想法。

✨总结

线程安全是什么?

确保多个线程访问同个资源最终结果的一致性。

怎么保证线程安全?

加锁&依赖内存屏障。保证同一资源同时操作的线程只有一个和解决「内存可见性」&「指令重排问题

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 5:01:56

计算机毕业设计之springboot闲置摄影器材换购平台的设计与开发

系统根据现有的管理模块进行开发和扩展&#xff0c;采用面向对象的开发的思想和结构化的开发方法对闲置摄影器材换购的现状进行系统调查。采用结构化的分析设计&#xff0c;该方法要求结合一定的图表&#xff0c;在模块化的基础上进行系统的开发工作。在设计中采用“自下而上”…

作者头像 李华
网站建设 2026/5/1 7:11:47

淘宝客返利系统的用户数据安全设计:脱敏存储与接口访问控制

淘宝客返利系统的用户数据安全设计&#xff1a;脱敏存储与接口访问控制 大家好&#xff0c;我是 微赚淘客系统3.0 的研发者省赚客&#xff01; 在淘宝客返利系统中&#xff0c;用户数据的安全性至关重要。用户手机号、身份证号、支付宝账号等敏感信息一旦泄露&#xff0c;不仅…

作者头像 李华
网站建设 2026/4/19 0:09:24

无需编程!轻松打造全能活动报名与表单收集系统源码

温馨提示&#xff1a;文末有资源获取方式 在当今数字化的商业环境中&#xff0c;高效的信息收集和活动管理成为各行各业的核心需求。为此&#xff0c;我们隆重推出一款基于先进技术开发的万能活动在线报名自定义表单系统源码。该系统设计灵活&#xff0c;功能全面&#xff0c;能…

作者头像 李华