volatile关键字解析
由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理.
内存模型相关概念
在计算机执行程序时,每一条指令都是在CPU中执行的,而执行指令的过程中就会牵扯到数据的读取和写入.但是在程序运行的时候临时数据是储存在物理内存中的,那么就会面临一个问题,由于CPU的运行速度很快,但是在内存中读取和存储数据相比于CPU执行指令来说那会慢很多的,那么我们就来想一想如何能改善一下这个现状.
分析:
1.造成原因是因为:数据读取,储存赶不上CPU处理指令的速度.
2.假使俩个人差距太大,如何才能缩小差距,最好的办法有一个中间人,对差的人有帮助提升,对优秀的人积极向其靠拢.
3.那么高速缓存就及时出现了
高速缓存
程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
例子
i = i + 1;
当线程执行这个语句时,会先从主存当中读取i
的值,然后复制一份到高速缓存当中,然后CPU执行指令对i
进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。
在单线程中,这个模式是没有一点问题的,但是在多线程中,就会出现一些问题,我们都知道在多核CPU中,每一条线程都位于不同的CPU中,那么,就会有资源抢占
的问题.如下图解:
站长说:“我们现在有100张票,你们谁先买完我就提拔谁当站长”
那么作为有晋升理想的售票员,那肯定会把这一部分票卖的愈多愈好,但是面对顾客的时候就会出现问题.就比如说我有多个顾客在不同的窗口购买票据,但是因为售票员理解错误以为各自都有100张票的权限,可想而知,最终顾客会买到 同一张票的可能性会很大.为什么?(注意我之前提到过多个顾客)
那么遇到这种资源抢占的问题时,我们有以下俩种方式进行解决
- 通过在总线加LOCK锁的方式
- 通过缓存一致性协议
这俩中方式都是硬件层面上提供的方式.
在以前CPU中,我们习惯在总线程上面添加Lock
锁来解决缓存不一致的问题,但是对总线程加锁的话势必会阻塞其他CPU对其他指令的访问从而造成CPU只能为当前的指令服务.那就好比说,上述实例,我只有一个售票窗口,那么我就不会出现购票冲突了.但是这样造成的效率问题同样也很大.
相应的就出现了缓存一致性 最出名的就是Intel 的MESI协议,这个协议说白了就是当CPU写数据时发现当前操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
并发编程中的三个概念
在并发编程中我们经常会遇到以下三个问题:
- 原子性问题
- 可见性问题
- 有序性问题
1. 原子性
解释
原子性:就是一个操作或多个操作 要么全部执行并且执行的过程中不会其他行为打断
很经典一个例子就是 银行取钱这个示例,试想一下这些操作都没有原子性,比如A要转钱100给B,结果中途停了,那么由于向B转钱的这个事件已近发生但是A账户并没有进行转钱这一行为(导致A账户还有钱),所以A在取20也是可以的.那么大家想一想,这20元的损失由谁来承担.
所以事务必须具有原子性
2.可见性
可见性,就是指的是多个线程访问同一个变量的时候,一个线程改变了变量的值,其他线程能够立即看到修改的值.
解释
//CPU执行线程的代码片段1
int i = 0;
i = i + 1;
//CPU执行线程的代码片段2
w = i;
假使现在这种状态下代码片段1
是由CPU1执行的,代码片段2
是由CPU2执行,那么当CPU执行代码片段1
部分时,我们会将i = 0 + 1
的值赋值给高速缓存,但是当CPU赋值给高速缓存中,此时还没写到内存中.那么CPU在执行w = i
时候,由于高速缓存没有将数据写入内存中,所以数据还为0
.
这就是可见性问题,代码片段1
对变量修改之后,代码片段2
没有立刻看到代码片段1
修改的值.
3.有序性
有序性:即程序执行的顺序按照代码的先后顺序执行
要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
解析volatile关键字
1. volatile关键字的两层语义
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。