多线程 volatile
多线程关键字volatile
从硬件层面理解 volatile 的原理
volatile是解决多线程使用同一数据时无法保证一致性的问题。比如下面的代码
public class ClassSync implements Runnable{
@Override
public void run() {
for (int j = 0; j < 10; j++) {
System.out.println(Thread.currentThread().getName()+" i:"+i++);
}
}
}
// 执行
public class Main {
public static void main(String[] args) throws InterruptedException, IOException {
ClassSync sync = new ClassSync();
new Thread(sync,"thread_a").start();
new Thread(sync,"thread_b").start();
}
}
观察输出,可知输出的数据是错误的
thread_b i:0 # b输出 0
thread_b i:1
thread_b i:2
thread_b i:3
thread_b i:4
thread_a i:0 # a输出0
thread_a i:5
thread_a i:6
thread_a i:7
thread_b i:8
thread_a i:9
thread_a i:11
thread_a i:12
thread_a i:13
thread_a i:14
thread_a i:15
thread_b i:10
thread_b i:16
thread_b i:17
thread_b i:18
Process finished with exit code 0
这种如果不能从锁来解决,那就只能从变量入手了。
铺垫:Java是一门支持多线程的语言,CPU有4核,有8核,有16核等等,在每个时刻,一个CPU内核只能执行一个线程,但这并不是说,一个CPU内核只能执行一个线程;当前线程执行完成后,操作系统会调度其他线程上CPU执行,或当前线程阻塞后,CPU会切换内核上下文给其他线程执行。
CPU缓存
大多数CPU带缓存
- 一级缓存L1:供内核使用,每个核心都拥有2个,速度最快的缓存
- 二级缓存L2:供内核使用,每个核心都拥有1个,存储容量比一级缓存大
- 三级缓存L3:所有核共享使用,速度更慢,空间更大。
CPU的对数据计算速度远远高于内存的吞吐量,所以加入了缓存用以尽可能的减少CPU与内存之间数据的交互,保证CPU的高速执行。Java线程在并发执行的前提下,每个线程需要通过总线拿内存中的数据,拿数据这一动作是串行的,a线程拿完,b线程才能接着拿,这由CPU的总线锁控制。
总线:CPU总线相当于运输通道,保证内存与内核,内核之间的数据交流。内核中的线程拿数据时先到其他内核缓存查,查不到再去内存中拿。总线细分总线有3种,感兴趣可自行下去查阅资料。
MESI协议
大多数CPU使用MESI协议,这项协议用于标识缓存中数据与内存中数据状态
MESI协议
- M:被修改,当前缓存中的数据与内存中不一致
- E:独享,数据只缓存在内核专属缓存中,并且没有被修改
- S:共享,代表所标识的数据已经在多个内核的专属缓存中存在,并且和主内存中数据一致
- I:无效,标识的数据在当前内核专属缓存中无效
当A内核中的线程修改a的值时,它首先会去自己的专属缓存中拿数据,若没有,再去内存和其他内核的专属缓存(B核,C核,D核)中拿,拿到数据后存入此A核专属内存中,此时a数据是E状态,随后A内核的线程修改其值,但并未回写入内存,此时数据状态变为M,随后B内核也需改a的值,它首先会去其他各个核的专属缓存中访问一遍,发现A核的专属缓存中存在a的值,于是B核通过总线通知内存修改数据,B核把数据拷贝到自己的专属缓存中,随后通知其他核把a数据的状态变为S;B核线程修改a的值,这个时刻,B核缓存中的数据与内存不一致,B核通知内存更新,通知其他内核将它们缓存中的a的状态改为I,B核缓存内的a状态改为E,当A核线程继续修改a时,发现自己的缓存中的a是I状态,于是又会通过总线访问其他核的专属缓存和内存,发现B核有a,且状态为E,拷贝此值,并修改a的状态为E,通知B核将状态改为E;以此循环往复。
store buffer
和invalidate queue
但是协议存在一个bug,那就是所有线程都是串行地交互,在状态未更新完成前,其他涉及a数据的线程不可以做其他事。这显然影响性能,所以CPU引入了store buffer
和invalidate queue
,让内核线程处理数据的时候,能够做其他操作。举个例子,当一个班级的同学都在投票的时候,我可以做看书,发呆这些并不影响投票的活动(打破程序的有序性)。或者用代码举例
例
public int add(){ this.a = 10; int b = 20; return a+b; }
上面这段代码被多个线程执行,所有线程都在修改a值,a就肯定是个需要通知到各个内核线程的变量,在线程等待a赋值时,它可以先做b=20这一步,这里看出在实际的底层操作中a并没有完成自己的赋值操作,而是等待更新,内核中线程先做了b=20这一步,却不能做a+b,因为这涉及到与a相关的计算
总线把需要变更的数据给到invalidate queue
,CPU会慢慢同步到各内核的缓存中。
现在引发数据不一致性的导火线出现了:store buffer
和invalidate queue
,由于线程执行的时候,CPU是慢慢同步数据状态的,可能实时的状态没有同步到A内核的缓存里,A核内缓存中的数据被误视为最新的,随即被A核中的线程拿去执行了,错误就此产生!
为了避免这一情况,Java引入了关键字volatile
用于使store buffer
和invalidate queue
无效化,严格意义上也不能说是无效化,在执行运算操作前,检查store buffer
和invalidate queue
中没有更改通知后再进行运算操作。这不就回到了之前的串行通知了吗,是的!关键字volatile
解决了可见性和有序性,但会对性能造成影响。
store buffer:内核和缓存之间加一个store buffer,内核可以先将数据写到store buffer,同时给其他内核发送消息,然后继续做其它事情,等到收到其它内核发过来的响应消息,再将数据从store buffer移到缓存
可见性:一个线程的修改,对其他线程总是可见的
有序性:程序按照代码的先后顺序执行
本文来自博客园,作者:勤匠,转载请注明原文链接:https://www.cnblogs.com/JarryShu/articles/18185622
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现