并发编程JMM&Volatile底层原理剖析

目录

1. 一个简单的JMM代码题

2. 摩尔定律与计算机缓存架构

3. java内存模型和可见性问题

4. JMM的数据原子操作

 我们通过代码来进行描述

5. volatile关键字实现可见性

如何使用代码深入解释?

6. 双向检查锁定的单例需不需要加Volatile?

6.1 什么是双重检查锁定?

6.2 CPU的乱序执行

扫描二维码关注公众号,回复: 15680244 查看本文章

7. Volatile关键字的总结

实现可见性:

禁止指令重排序:

8. CacheLine与伪共享问题

9. CAS底层理解

10. JVM调优的本质

11. JDK怎么解决伪共享问题?

12. JMM的数据原子操作是怎么回事?

13. Volatile的底层实现原理


1. 一个简单的JMM代码题

运行耗时是相差无几的!

这里是和java的内存模型有关系的 也就是JMM

2. 摩尔定律与计算机缓存架构

摩尔定律:大概18个月CPU的速度会翻一倍!内存的速度增长每年之后8%左右

这个时候内存和CPU的性能差距就会越来越大,后来为了弥补差异,就引入了一个新的概念,Cache【高速缓存】

数据越小的离CPU越近

3. java内存模型和可见性问题

所谓可见性问题就是,在java内存模型中,保存的都是变量副本,当一个线程修改了某个数据之后对于另外一个线程来说他是不知道的!

4. JMM的数据原子操作

 我们通过代码来进行描述

 会一直停留在线程1里面

当前是不可见的,但是如果我们添加volatile这个关键字的话就会使得数据可见了!

5. volatile关键字实现可见性

volatile是怎么实现可见性操作的?

步骤如下所示:
①我们在线程2进行了修改,修改之后由于volatile机制,就会让修改后的数据立马写回给主内存!

②写回给主内存后就会触发总线嗅探机制,此时线程1发觉到flag的值已经修改,也就是说flag的值已经发生了变化,那么就说明原来的值已经过期了,这个时候会将工作内存中的数据清空

③此时由于线程1的工作内存里面没有了指令,就会重新进行read--load操作,重新从主内存中获取数据

【所以,volatile之所以可以实现就是基于总线嗅探机制进行的】

如何使用代码深入解释?

我们可以从汇编层面进行查看 

下载插件hsdis,然后查看lock前缀,最后找到我们想要找寻的对应的汇编语言

6. 双向检查锁定的单例需不需要加Volatile?

6.1 什么是双重检查锁定?

首先我们都知道单例模式存在两种 ---- 懒汉式和饿汉式

饿汉式是在最开始的时候就已经创建好了我们需要的对象并分配内存空间,但是会存在一个问题就是如果我们一直不使用就会造成内存一直被占用,浪费资源!

所以我们采用了懒汉式,但是懒汉式存在的缺点就是线程不安全,面对线程不安全我们如何解决? ---- 加锁

我们一般不推荐在方法上加锁,一般都是使用方法体然后进行加锁,如下面的代码所示

那么就下面的代码来说,我们中间的第二次判断的if(object==null)可不可以去除?

答案是:当然不行!!!

我们可以来模拟一下:

 

所以双重检查锁定就是单例模式下,两次条件判断并添加锁!

但是DCL还是还是存在一些问题 ----- 拿到的数据并不准确!

怎么来说这个问题呢?我们首先来看一个概念

6.2 CPU的乱序执行

 有了上面的概念,我们开始验证我们的结论

我们都知道正常的开发的步骤是: new  --》  init  --》  aload

但是如果在实现的时候,CPU调用了指令重排序的话,就会打乱顺序,假设现在变为new --》 aload  --》 init

那么在实现的时候会发生什么,我们现在假设有两个线程同时操作

 这个时候线程2拿到的对象是一个半初始化的对象

我们知道在new的时候,对一些数据就new出对应的初始值,比如int类型的初始值为 0 ;boolean类型的初始值为false这种,所以这个时候i的值就是0,那么后面所有有关于i的时候使用的数据都是0,但是!我们最开始本来就已经有对对应的数据赋值了,那就会造成数据不对的情况了  

所以在双重检查锁定的单例模式下就会出现拿到的数据异常的情况!也就是拿到的数据是半初始化的数据!

那么怎么解决???

这个时候就需要volatile来进行解决了。也就是我们所说的双重检查锁定的单例模式下需要使用volatile关键字!!

volatile可以禁止指令重排序!!

7. Volatile关键字的总结

Volatile关键字有两个作用:禁止指令重排序+实现可见性

实现可见性:

当我们两个线程进行操作的时候,比如现在线程1执行的是读取操作,线程2执行的是修改操作

我们用代码来做简单的说明:【这里表示俩个线程分别是做什么的】

我们都知道在JMM如下所示

这个时候关键字Volatile的作用就来了,它可以实现可见性,它添加了一个总线

 一旦数据变化就会立刻会写到主内存,并且会触发总线嗅探,修改其他线程中的工作内存中的数据!

步骤如下所示:
①我们在线程2进行了修改,修改之后由于volatile机制,就会让修改后的数据立马写回给主内存!

②写回给主内存后就会触发总线嗅探机制,此时线程1发觉到flag的值已经修改,也就是说flag的值已经发生了变化,那么就说明原来的值已经过期了,这个时候会将工作内存中的数据清空

③此时由于线程1的工作内存里面没有了指令,就会重新进行read--load操作,重新从主内存中获取数据

 这样就实现了可见化

禁止指令重排序:

然后大家都知道CPU存在自动重排序!!!

这个排序会应用在没有使用Volatile的操作里面就会造成数据异常,可以看看我上面的描写,那么Volatile关键字加进来之后就可以完美的解决这个问题!!!!

8. CacheLine与伪共享问题

我们再回到我们最开始的那个问题

为什么两个代码访问的数据量相差16倍,但是时间却相差无几?

这就和CacheLine有关,在上面的代码里面,访问的缓存行数其实还是一样多的!!

【一个int占用4个字节,16个int又会占用一个CacheLine】

请问是不是所有的地方都可以添加Volatile?  ---》 并不是!!!

比如在这个共享的位置,如果我们添加关键字Volatile的话,就会出现什么情况?

添加之后我们就会发现,我们会启动Volatile的可见化,然后就会执行局部内存的刷新并执行总线嗅探重新获取数据!

这样就会造成性能很慢很慢!!! 也就是会形成我们所说的伪共享!!

如果我们一定要添加Volatile的话我们应该怎么办?

我们可以使用添加注解的方式进行解决 :@sun.misc.Contented【这样我们就会解决伪共享的问题了!!】

9. CAS底层理解

CAS:compare and swap  比较并交换  ----  乐观锁

10. JVM调优的本质

复制、标记清除、标记整理三种方法里面,复制的效率最高!!

绝大部分对象朝生夕死,所以使用复制算法效率最高!

11. JDK怎么解决伪共享问题?

空间换时间


添加注解@sun.misc.Contended

注意在使用的时候还需要添加一个参数 才可以使用

12. JMM的数据原子操作是怎么回事?

用来完成JMM的运转

13. Volatile的底层实现原理

 

MESI协议:缓存一致性协议

怎么手写实现Volatile关键字?

在底层汇编代码前面添加lock前缀指令

猜你喜欢

转载自blog.csdn.net/young_man2/article/details/126670619