并发编程可以说是在java体系里面比较主要的一环,并发学好了基本上可以说是可以解决很大一部分问题,但是很多人都对并发侃侃而谈,那么我们就不得不说一下volatile这个关键字。
在说volatile之前我想说一下并发是三大特性:可见性,有序性,原子性。
下面我们就围绕三大特性展开谈谈。
我们都知道volatile可以实现可见性,有序性,但是不能保证原子性,那么volatile是怎么保证可见性的呢?
上一篇文章我们说到cpu多级缓存的问题,这里volatile就是利用cpu的缓存一致性原则(MESI协议)实现对变量进行修改可见的操作。
MESI为一种状态 :M:修改 E:独享 互斥 S:共享 I:无效
MESI工作原理:存在多个CPU的情况下 每一个CPU在其他CPU获取主内存中的数据时都会有一个嗅探机值(判断其他cpu有没有操作此数据) 一开始拿到主内存中的数据将数据修改为E(独享),当其他CPU也要去那数据时将数据修改为S(共享)状态,当一个CPU在寄存器中计算数据将数据状态改为M(修改)状态(此时会锁住缓存行(自己的线程里面的)(最小单元为缓存行)))同时向总线发送一个消息, 其他CPU通过嗅探机制将自己缓存内的数据改为I(无效)状态, 缓存一致性原则就是通过这样的机制去。
说到这里,有些人可能就要说到如果两个同时向总线发送修改的信号怎么办呢,那么此时就需要依靠总线来判断了(大概判断逻辑可能就是通过高低电位)。如果两个CPU同一时间要去修改数据状态此时还要一种机制(在指令周期内进行裁决将另外一个置为I(无效)状态)。
下面重点说一下在程序执行过程中遇到volatile关键字会发生什么?
当程序遇到volatile的时候其实会在底层对变量前加一个lock前缀,从而触发MESI协议去保证可见性。
而对应volatile保证有序性的问题,那么我们就需要说到内存屏障的问题。
内存屏障基于八大原子操作:lock-->read-->load->user->assign(赋值)->store(存储)->write->unlock,这八步原子操作必修要顺序执行,而且必须走完(而且每一步保证为原子性cmpchxg汇编指令),但是可以不是连续执行 但是注意 read和load store(存储 )->write必须同时执行。
下面举一个例子:如果创建线程count+一千次 可能最后得到的结果不是1000,因为多个线程由于缓存一致性的原因当一个cpu在寄存器add count时不一定会立刻写回主内存,这就导致其他线程拿到不是最新的count,如果count+下面还有代码逻辑,其他线程会继续执行,会指令重排,这样也会带来一个问题,会出错,所有又有一个内存屏障保证哪里不需要指令重排
说到有序性还要提到一个概念:cpu禁止指令重排
指令重排:在程序运行过程中可能会跨过某一段程序而继续去执行,但是不能改变最后程序运行的结果,指令重排CPU需要遵循 as-if-serial :不管怎么排序,程序最后结果不能改变 内存屏障去保证禁止指令重排。
指令重排发生在两个阶段:
1 发生编译时期 (字节码指令被翻译成机器码的时候加载class文件的时候 不是在java代码翻译成字节码的时候)
2 CPU运行时(执行汇编语句的时候)
而volatile保证有序性:禁止指令重排序优化。是基于内存屏障(4种:storestore storeload loadload loadstore)
我们可以想一下volatile为什么不能保证原子性:因为volatile是分为两步操作 先读再写 因为虽然有MESI机制,但是也会存在当一个CPU改了数据, 虽然另外一个CPU 通过嗅探机制将自己内存数据改为I(无效状态) 但是可能已经将数据放入自己的CPU寄存器里面了,此时两个CPU再同时写回主内存就会出现问题。
所以说volatile只能保证可见性和有序性,那么并发是三大特性一个都要满足,那么原子性怎么去保证呢,在一般情况下要保证原子性需要借助synchronized、Lock锁机制,同理也能保证有序性与可见性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块。
后面笔者也会着重去说一下关于lock体系