谈volatile关键字之前,首先必须聊聊JMM内存模型!
JMM主要的特性:可见性、原子性,顺序性
Java 虚拟机规范试图定义一种 Java 内存模型(JMM),来屏蔽掉各种硬件和操作系统的内存访问差异,让 Java 程序在各种平台上都能达到一致的内存访问效果。简单来说,由于 CPU 执行指令的速度是很快的,但是内存访问的速度就慢了很多,相差的不是一个数量级,所以搞处理器的那群大佬们又在 CPU 里加了好几层高速缓存。在 Java 内存模型里,对上述的优化又进行了一波抽象。JMM 规定所有变量都是存在主存中的,类似于上面提到的普通内存,每个线程又包含自己的工作内存,方便理解就可以看成 CPU 上的寄存器或者高速缓存。所以线程的操作都是以工作内存为主,它们只能访问自己的工作内存,且工作前后都要把值在同步回主内存。
网上找的一些定义:
简单描述如下:
- 主存存放线程需要操作的变量,但线程并不直接操作主存。
- 每个线程读取主存变量都是先拷贝一份到工作内存中,不同线程工作内存互不干扰。
- 线程修改了工作内存后,再写回主存中。
- 每次从主存读写的过程都需要经过原子性操作。
简单知道了java的内存模型,那开始聊聊volatile关键字。volatile关键字有三大特性:
①可见性
②不保证原子性
③禁止指令重排
首先要知道volatile是java虚拟机提供的轻量级的同步机制,volatile的可见性是由jvm发送一条lock前缀的汇编指令实现的。volatile关键字是修饰成员变量的,也就是说,如果一个成员变量加了volatile关键字,就会告诉编译器和jvm的内存模型,这个变量是对所有线程共享的,可见性每次JVM都会读取最新写入的值并使其最新值在所有的cpu可见。volatile可以保证线程的可见性并且提供了一定的顺序性,但是无法保证原子性。在JVM底层volatile是采用内存屏障来实现的。使用了volatile关键字就好像是线程直接操作了主内存。
为什么不能保证原子性呢?
在我们的程序中,即使加了volatile也不能保证原子性,有一种粗暴的解决办法,就是加我们的synchronized同步锁,但是这种锁太重了。简单画个图演示为什么不能保证原子性。
在程序中的i++操作,是不能保证原子性的.
那原子性和可见性不是就冲突了吗?不冲突
因为volatile 的可见性只能对应l原子性, a=1是原子性,而a++实际上是a=a+1 是非原子性的,所以会导致你说的情况,这时候就要引入同步,强制将a++转化为原子性。
那除了synchronized还有其他解决办法吗?
那就是大名鼎鼎的JUC包下的AtomicInteger,写一小段程序说明:
AtomicInteger atomicInteger=new AtomicInteger();
atomicInteger.getAndIncrement();//获取在增加
那为什么用AtomicInteger就能保证原子性呢?因为他的底层是CAS,接下来就要说说什么是cas了。
cas是compare and set的缩写,比较并交换,unsafe是cas的核心类,cas的底层是靠的unsafe类来保证原子性的,unsafe是jvm下的rt.jar的运行jar包里面。
跟进源代码可以看见这段代码:
public final int getAndIncrement(){
return unsafe.getAndAddInt(this,1);//这个1是值的valueoffset:内存地址偏移量
}
还靠的是一段do while循环,顾名思义,又叫自旋锁
这时候就应该思考,为什么在没有加锁的情况下,还能保证线程安全呢?
①因为atomicInteger借助了UnSafe提供的CAS操作能够保证数据更新的时候是线程安全的,那么为什么借助了unsafe就能保证线程安全呢?以下总结出自我的笔记
②
因为cas并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现汇编指令,这是一种完全依赖的硬件功能。通过他实现了原子操作。并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题,保证了线程安全。