对volatile的理解--从JMM以及单例模式剖析 (2)

共同测试num和atomicInteger,此时执行主函数,三次结果为

//第一次 main finally num value is 19217 main finally atomicInteger value is 20000 //第二次 main finally num value is 19605 main finally atomicInteger value is 20000 //第三次 main finally num value is 18614 main finally atomicInteger value is 20000

我们发现volatile关键字并没有保证我们的变量的原子性,但是JUC内部的AtomicInteger类保证了我们变量相关的原子性,AtomicInteger底层用到了CAS,CAS不了解的话可以参考这篇文章

1.3 禁止指令重排

有序性的概念:在计算机执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分以下三种

image-20210704235910636

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。

处理器在进行重排顺序是必须要考虑指令之间的数据依赖性

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性时无法确定的,结果无法预测

重排代码实例: 声明变量: int a,b,x,y=0

线程A 线程B
x=a;   y=b;  
b=1;   a=2;  
执行结果   x=0,y=0  

如果编译器对这段程序代码执行重排优化后,可能出现如下情况:

线程A 线程B
b=1;   a=2;  
x=a;   y=b;  
执行结果   x=2,y=1  

这个结果说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的

volatile实现禁止指令重排,从而避免了多线程环境下程序出现乱序执行的现象

内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,他的作用有两个:

保证特定操作的执行顺序

保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

由于编译器和处理器都能执行指令重排优化。如果在之间插入一条Memory Barrier则会告诉编译器和CPU, 不管什么指令都不能和这条Memory Barrier指令重排顺序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读 取到这些数据的最新版本

image-20210705000533490

2.JMM(java内存模型)

为什么提到JMM?JMM当中规定了可见性、原子性、以及有序性的问题,在多线程中只要保证了以上问题的正确性,那么基本上不会发生多线程当中存在数据安全问题

JMM(Java Memory Model)本身是一种抽象的概念,并不真实存在,他描述的时一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM关于同步的规定:

线程解锁前,必须把共享变量的值刷新回主内存

线程加锁前,必须读取主内存的最新值到自己的工作内存

加锁解锁时同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有的成为栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是贡献内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先概要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存的变量副本拷贝,因此不同的线程件无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,期间要访问过程如下图:

image-20210704235759883

JMM的三大特性

2.1可见性

2.2原子性

2.3有序性

所以JMM当中的2.1和2.3在volatile当中都有很好的体现,volatile关键字并不能保证多线程当中的原子性,但是volatile是轻量级的同步机制,不想synchronized锁一样粒度太大

3.你在那些地方用过volatile?结合实际谈论一下?

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzdyss.html