news 2026/4/18 7:30:14

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

作者头像

张小明

前端开发工程师

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/4/13 20:03:38

计算机毕业设计之ssm基于Android的新闻平台设计与实现

时代在飞速进步&#xff0c;每个行业都在努力发展现在先进技术&#xff0c;通过这些先进的技术来提高自己的水平和优势&#xff0c;APP的新闻平台当然不能排除在外。APP新闻平台是在实际应用和软件工程的开发原理之上&#xff0c;运用java语言以及ssm框架进行开发。首先要进行需…

作者头像 李华
网站建设 2026/4/16 1:14:43

解锁LLM新能力!Engram:用条件记忆重塑大语言模型架构(THS)

解锁LLM新能力!Engram:用条件记忆重塑大语言模型架构 当MoE与N-gram相结合,一种全新的稀疏性范式正在悄然改变大语言模型的能力边界。 近期,来自北京大学和深度求索的研究团队提出了一种名为Engram的创新条件记忆模块,它通过结合经典的N-gram嵌入技术和现代深度学习架构,…

作者头像 李华
网站建设 2026/3/30 13:56:49

农业大数据系统怎样在富文本编辑器中嵌入Excel动态图表?

《Word转存大作战&#xff1a;一个穷学生的CMS升级日记》 一、需求分析与绝望的开始 作为一名月生活费2000还要养女朋友的计科狗&#xff0c;当我看到产品经理&#xff08;其实就是我自己&#xff09;提出的需求时&#xff0c;手里的泡面突然不香了&#xff1a; 核心需求&…

作者头像 李华
网站建设 2026/4/17 0:36:55

nodejs门店商铺店铺租赁租凭平台的设计与实现-vue

文章目录系统架构设计核心功能模块技术亮点性能优化安全机制--nodejs技术栈--结论源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;系统架构设计 采用前后端分离架构&#xff0c;前端基于Vue.js框架实现用户界面&#xff0c;后端使用Nod…

作者头像 李华
网站建设 2026/4/18 3:31:07

边界值优化在AI翻译测试中的应用:破解术语一致性难题

术语一致性的测试困局 在全球化软件产品的本地化测试中&#xff0c;术语一致性缺陷已成为AI翻译系统的核心痛点。传统测试方法面对多语言场景时&#xff0c;常因术语歧义&#xff08;如"server"被交替译为“服务器/伺服器”&#xff09;、动态语境适应失效等问题&am…

作者头像 李华
网站建设 2026/4/18 3:34:49

‌Appium移动端兼容性测试:结合大模型回归验证策略

兼容性测试的挑战与机遇‌移动应用兼容性测试是确保应用在不同设备&#xff08;如Android/iOS&#xff09;、系统版本、屏幕分辨率和网络环境下稳定运行的关键环节。传统方法依赖人工遍历测试用例&#xff0c;效率低且易遗漏边缘场景。例如&#xff0c;Android碎片化问题导致需…

作者头像 李华