请谈谈你对volatiel的理解?
文章目录
volatile是什么
首先JUC指的是java的三个包:
首先这个关键字在你日常的单线程工作环境下你是用不到的。
它的3大特性:
JMM内存模型之可见性
JMM是什么
读取速度而言,硬盘<内存,磁盘的io达到了一定的阈值之后,很难突破,除非硬件有突破。所以,部分数据,别存在mysql了,存在内存上,比内存再快的就是cpu(这里的快不是读取的概念了)。如果cpu计算的快,内存来不及喂,就不能完全发挥性能,这时候就需要在cpu和内存之间有个cache(缓存)。这里可以参考一下cpu-z软件里面“缓存”的概念。
假设多个线程,读取主内存(理解为你的8g内存条)上的变量。每个线程都有自己的工作内存,会把变量拷贝回自己的工作内存中,主物理内存中的不动,然后各个线程修改自己的变量。修改完了之后,再写回主物理内存,但是写回去之后,其他线程并不知道你改完之后变量的值,还是拿的旧变量的拷贝。这个数据同步问题就需要解决了。这个及时通知的需要,就称为JMM的内存“可见性”。
因为通信传值必须通过主内存完成,所以无法线程间进行访问拷贝变量的访问和修改。所以工作内存中改完之后要及时写回主内存空间,然后通知其他线程,这种及时通知的特性就是jmm的可见性。
JMM三大特性-可见性的代码验证说明
谈谈JMM,如下图:
对比volatile的三大特性,JMM的特性他遵守了两个,不保证原子性,原子性可以通过sync关键字保证。
代码验证
如果可见性被触发,那么main线程会结束。不加volatile,main会一直等待。也就是说main没有看到其他线程的修改。
只要加入volatile关键字,就不一样了。增加了JMM的可见性。
volatile不保证原子性
可见性描述:核心是及时通知
原子性是什么意思?
案例演示:
原子性就是保证最终的一致性。操作不要被砍成多段。
执行结果:
很明显,没有满足原子性,最终的计算结果根本不对!
不保证原子性的理论解释
因为不保证原子性,就会出现丢数据的情况。
最简单的解决办法,是把并发变成单向车道,也就是加上同步锁,如下:
但是sync,是大材小用了,太重量级了,性能低。
为什么会出现原子性问题?看下图
多线程场景,3个线程都把变量的快照读到了自己的工作空间,当1号线程,改掉了变量值,要写回主内存的时候,被挂起,此时2线程执行修改的操作,并写回了主内存,此时1线程恢复,并继续执行,把修改好的数据写回到主内存,它并不知道主内存中的变量值被修改过了,就出现了对2线程修改结果的覆盖,也就造成了丢值的现象,最终的结果就不准了。
虽然是i++,一句java代码,但是class字节码却不只1行,执行过程中,在多线程环境下,是会被挂起,中途执行中断。
看一下i++ 的字节码:
其中的字节码解释:
这个发生过程是这样的:线程123都拿到了主内存的n值,都执行iadd加1操作,这个过程发生在工作内存,而不是主内存,所以操作完了之后,要写回去,执行putfield方法,正常应该是,顺序读写,这时候最终结果就是对的,但是如果有一个线程被挂起,另外两给被唤醒继续执行,这个过程十分快,就容易拿不到最新的值,值写丢了,产生了覆盖。
不保证原子性的解决方案
volatile是轻量级的同步机制,乞丐版的sync,不能保证原子性,想要解决这个问题,方案为:
- sync同步锁
- JUC包提供的原子类atomic。
代码案例:
mydate类中的变量:原子类的操作,先获得再++,就不会出现上述的情况了,整个过程是不会被中断的。
测试代码:
执行结果:
为什么原子类可以解决?因为底层是cas算法实现的。说一下cas,围绕自旋锁和unsafe类讲一下。
指令重排案例1
有序性(指令重排):
数据依赖性:举个例子,要先定义,再赋值,这叫数据依赖性。定义一定在赋值执行的前面。
这个其实很好理解,考试的时候,你做题的顺序,不一定是按照题号来,肯定是先做简单的,再做难得,一切为了得高分(性能好),这就是指令重排。
案例一:
正常顺序为1234.因为指令重排会变成:
需要注意的是,因为数据依赖性,不可能出现4123。
案例2:
这种重排,对业务产生了影响了,就必须得禁止。
指令重排2
指令重排,编译器认为这是种优化,但是多线程环境下,可能会对业务产生产生影响。
这时候就需要用volatile禁止重排!
案例3:
如果是单线程,执行了method1,再执行method,a是6.
但是如果是多线程发生了指令重排,因为线程交替执行的存在,可能先执行flag=true,之后被挂起,然后其他线程切入执行method2,此时打印的a是5,结果就错了!
总结:满足JMM三大特性之后,多线程场景就可以理解为线程安全的。
多线程下单例模式存在安全问题
懒汉式
简单单线程环境测试:
证明是一个对象。
多线程下:
但是结果是:
可以看出,多线程环境下,传统的懒汉式就出问题了。
解决方案1:加sync关键字。
就可以解决了,但是,性能差,对并发并不友好,强迫单车道执行。
解决方案2:DCL模式,即双端检查锁机制。
加锁的前后,都做一次判断。所谓的双重检查锁。
测试也是没有问题的。
到这,DCL版本的单例,还没有写完!为啥,因为刚才讲的,在多线程环境下,可能发生指令重排!!!!
所以,因为指令重排,会发生其他线程取值时候还没初始化完成的情况,就线程不安全了,没有满足instance的原子性。
所以需要volatile保证多线程之间的语义一致性。