查看原文
其他

JMM的八大原子操作就是这么简单!

IT服务圈儿 2022-09-11

The following article is from 程序员架构 Author 立体的萌

来源丨本文经授权转自 程序员架构(ID:chengxuyuanjg)

作者丨立体的萌

引子

可能有的人一看到JMM有点懵,对于我们搞Java的来说,JVM我们再熟悉不过了,可以说不懂JVM就不能称为一个合格的java程序员,可是JMM又是什么呢?


JMM不像JVM应用那么广泛,也不像JVM一样知名度那么高,很多人都不知道JMM甚至把他跟JVM混为一谈(毕竟长得这么像),知道的恐怕也不见得明白JMM的重要性,这都是非常错误的观点;


我们既然要学一个技术,就必须要明白为什么要学;


我喜欢用以终为始的态度来看待问题,也就是先搞懂重要性再学习,等于带着好奇心以及问题去学习;


JMM包含着很多并发编程的底层原理,比如各种锁、线程池以及原子操作等等,这些老朋友我们天天见,但是我们不知道他们怎么工作的;


我举一个很简单的例子,通常我们为了线程安全都会加上Volatile关键字,可他为什么可以保证线程安全?把java翻译成汇编语言就会发现相应的代码那里会发现“lock”,这一切都要从JMM中寻找答案;

通过CPU缓存架构来理解JMM

我们都知道摩尔定律,集成电路上可以以容纳的晶体管数目每过一年半就会增加一倍,也就说CPU的性能大约两年以内就会翻一倍;


CPU本身就是计算机所有的零件中速度最快的,而且也是唯一一个有着加速度的零件,那么,CPU一定会存在和其他零件速度不匹配的问题,为了解决这个问题,就增加了CPU缓存;


有一位计算机科学家说过,没有什么问题是抽象一层解决不了的,如果解决不了那就抽象两层;


以一个双核CPU为例,计算机的程序都是存放在硬盘上,如果计算机要执行程序的话,必须先把程序加载到内存,一开始CPU会直接从内存中读取数据,但是随着CPU的速度越来越快,内存都追不上了,也就是出现速度不匹配的问题了,最严重的后果就是CPU的高性能被内存严重制约了;


为了充分的发挥CPU的性能,就增加了CPU缓存,准确的说是CPU 高速缓存,现在计算机基本上都是这样设置的;


一图胜千言



JMM模型与CPU缓存架构高度相似,一图胜千言



来看一段代码实例



在这段代码中,initflag就是共享变量,我创建了两个线程,这两个线程同时读取initflag,他们把initflag都读取到自己的工作内存中,并形成共享变量的副本,我原本给initflag赋的值是false,第二个线程读取之后把initflag的值改为了true,然而第一个线程还啥也不知道呢;


一旦其中一个线程更改了共享变量的值同时也让其他线程感知到的话,那么线程就都会结束,整个程序也就结束了;


那如果想要结束程序要怎么样呢?这就要用到Volatile关键字了,在共享变量之前加上:



Volatile的官方解释是保证每个线程的共享变量副本的可见性,可是Volatile是怎么做到这一切的?以及我们耳熟能详的JMM原子操作,这些都是怎么回事?这就需要我们去探究底层原理了。

探究底层原理

其实,任何一个程序,一旦涉及到底层的交互操作,就拿刚才那段代码为例,内存的数据加载在线程的工作内存以及工作内存修改之后又是如何记载到内存的?还有很多很多,完成这些工作的就是JMM那8个数据原子操作;


结合之前的例子,我们来看一下JMM的数据原子操作的工作流程


  • 第一步-read:从内存读取数据



  • 第二步-load:将内存读取的数据写入工作内存



  • 第三步-use:从工作内存中读取数据并进行计算



我在写第一个线程的时候写了死循环,执行到这一步,如果不看别的,只有线程1自己在这里原地打转,我写的第二个线程才会涉及到复制的操作,在第四部展示赋值的时候我会把之前的三步都给补上;


  • 第四步-assign:将计算好的值赋值到工作内存中



然后进行赋值



  • 第五步-store:把工作内存的数据写入内存



  • 第六步-write:把存储起来的变量赋值给内存



此时,线程2把修改过的值也传入到内存中了,线程2也结束了,但是线程1还是继续待在自己的小世界里,为了解决这个问题我加了个关键字Volatile,Volatile实现了缓存可见性,其原理大致是通过汇编语言的lock指令来锁定该内存区域的缓存并会写到内存。


  • lock:将内存变量加锁,标识为线程独占状态


加锁的操作是在store之前的,因为在store之前加锁一个是为了保证不乱改数据,加了锁游戏规则就变成了谁先抢到锁谁就赋值;


另一个更重要的原因是出于性能的考虑,前三步是比较麻烦的,为了能够展示其底层操作步骤我尽量用的最简单的例子,实际上一个线程的工作可比这复杂的多得多!


拿最坏的情况来讲,如果在read操作之前加锁,会让后面的线程等待的时候过长,可能不用等到资源分配不均的情况出现,自己就死锁了,而赋值操作对于内存来说小菜一碟,其速度是按照纳秒来计算的,非常非常快,在assign后面加锁不会让后面的线程等太久,这样就可以大大的提升计算机的性能


为了更直观的体现,我把之前的图稍作修改再添加



  • unlock:将内存变量解锁,解锁后其他线程可以锁定该变量



本文作者:立体的萌 

个人网址:https://blog.csdn.net/weixin_46107282

声明:本文为 程序员架构 原创投稿,未经允许请勿转载。


关于作者:

笔名立体的萌,真实姓名刘梦陈,是个工作还不到一年就辞职考研的的菜鸟,上学学习的是java,但是工作用的是C#,C#用途远远没有java范围广,再加上java是我的母语,所以很想回归java,不仅仅是提升技术能力,也有往大数据等高端领域发展的想法。

 

请作者吃糖


end


👇👇👇👇👇赠书福利来袭啦 | 包邮送


参与方法

1、关注「程序员架构」公众号

2、查看6.6头条文章,留言说出你的观点看法。

也可以点击原文链接直达~~~


3、活动截止时圈儿会在留言区选取4名小锦鲤,获得上面的纸质书籍一本,免费包邮到家


1、好可怕的搜索引擎!

2、华为正式发布鸿蒙2.0,更新人数太多挤爆服务器,P50也官宣了!

3、9.3k Star!一个开源的现代化 Windows 文件管理器!

4、算法面试题:均分纸牌

识别关注我们

了解更多精彩内容

点分享

点点赞

点在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存