深入浅析Java中的volatile?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。
成都创新互联公司网站建设公司是一家服务多年做网站建设策划设计制作的公司,为广大用户提供了网站制作、网站设计,成都网站设计,广告投放,成都做网站选成都创新互联公司,贴合企业需求,高性价比,满足客户不同层次的需求一站式服务欢迎致电。前言
volatile
相关的知识其实自己一直都是有掌握的,能大概讲出一些知识,例如:它可以保证可见性;禁止指令重排。这两个特性张口就来,但要再往深了问,具体是如何实现这两个特性的,以及在什么场景下使用volatile
,为什么不直接用synchronized
这种深入和扩展相关的问题,就回答的不好了。因为volatile
是面试必问的知识,所以这次准备把这部分知识也给啃掉。
系统处理效率与Java内存模型
在计算机中,每条程序指令都是在CPU中执行的,而CPU执行指令的数据都是临时存储在内存中的,但是CPU的执行速度远超内存的读取速度,如果所有的CPU指令都是通过内存来读取数据的话那么将大大的降低了系统的处理效率,所以现代计算机系统都不得不加入一层或多层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲。
将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后,在从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。
虽然说增加了高速缓存提高了CPU的处理效率,但是也带来了新的问题 :
现代计算机都是多核CPU,一开始,内存中的变量A的值是1,第一个CPU读取了数据,第二个CPU也将数据读取到了自己的高速缓存当中,当第一个CPU对变量A进行加1操作时,变量A的值变成了2,然后将将变量A的值写回内存中,这时第二个CPU也对变量A进行加1操作时,由于第二个CPU中高速缓存中的值还是1,所以加1操作后的结果为2,然后第二个CPU又将变量A的值同步回内存中,这样就导致执行了两次加1操作后,变量A的值最终是2,而不是3。
这种被多个CPU访问的变量,通常称为共享变量。
而产生的上面的问题,就是引入了高速缓存后的,主内存和缓存内容不一致的问题。
因为每个处理器有自己的高速缓存,但是它们又共享同一块主内存,所以必然会出现主内存不知该以哪个高速缓存中的变量为准的情况。
上面这个缓存不一致的问题,我们先记下来,继续来看Java内存模型,其实Java内存模型描述的上面讲的计算机系统高速缓存和内存之间的关系类似。
Java内存模型描述了,各种变量的访问规则,以及将变量存储到内存和从内存读取变量的这种底层细节。
在Java内存模型中关注的变量都是共享变量(实例变量、类变量)。
所有的共享变量都是存储在主内存中的,但是每个线程在访问变量的时候也都会在自己的工作内存(处理器高速缓存)中保留一份共享变量的副本。
Java内存模型(Java Memory Model,简称JMM)规定:
线程对变量的所有操作(读,写)都必须在工作内存中进行,不能直接操作主内存中的数据。
不同线程之间 也不能直接访问对方工作内存中的变量,线程间的变量值传递必须通过主内存进行中转传递。
在JMM中工作内存和主内存的关系如下图:
Volatile的可见性(保证立即可见)
继续我们上面的缓存一致性的问题,这个问题,在Java内存模型中,就是可见性的问题,即一个线程修改了共享变量的值,对另一个线程来说是不是立即可见的。如果不是立即可见的,那么就会出现缓存一致性的问题,如果是立即可见的,那么另一个线程在进行操作的时候,拿到的变量值就是新的。就可以解决可见性的问题。
那么怎么解决可见性问题呢?
将共享变量加锁,无论是synchronized
还是Lock
都可以,加锁达到的目的是在同一时间内只能有一个线程能对共享变量进行操作,就是说,共享变量从读取到工作内存到更新值后,同步回主内存的过程中,其他线程是操作不了这个变量的。这样自然就解决了可见性的问题了,但是这样的效率比较低,操作不了共享变量的线程就只能阻塞。
当一个共享变量被volatile
修饰后,会保证每个线程将变量修改后的值立即同步回主内存中,当其他线程有需要读取变量时会读取到新的变量值。
那么volatile做了些什么操作就能解决可见性的问题呢?
被volatile修饰的变量,在被线程操作时,会有这样的机制:
就是线程对变量操作时会从主内存中读取到自己的工作内存中,当线程对变量进行了修改后,那么其他已经读取了此变量的线程中的变量副本就会失效,这样其他线程在使用变量的时候,发现已经失效,那么就会去主内存中重新获取,这样获取到的就只新的值了。
那么volatile
这个关键字是如何实现这套机制的呢?
因为一台计算机有多台CPU,同一个变量,在多个CPU中缓存的值有可能不一样,那么以谁缓存的值为准呢?
既然大家都有自己的值,那么各个CPU间就产生了一种协议,来保证按照一定的规律为准,来确定共享变量的准确值,这样各个CPU在读写共享变量时都按照协议来操作。
这就是缓存一致性协议。
最著名的缓存一致性协议就是Intel的MESI
了,说MESI时,先解释一下,缓存行:
缓存行(cache line):CPU高速缓存的中可以分配的最小存储单位,高速缓存中的变量都是存在缓存行中的。
MESI的核心思想就是,当CPU对变量进行写操作时发现,变量是共享变量,那么就会通知其他CPU中将该变量的缓存行设置为无效状态。当其他CPU在操作变量时发现此变量在的缓存行已经无效,那么就会去主内存中重新读取新的变量。
那么其他CPU是如何发现变量被修改了的呢?
因为CPU和其他部件的进行通信是通过总线来进行的,所以每个CPU通过嗅探总线上的传播数据,来检查自己缓存的值是不是过期了,当处理器发现自己换成行对应的内存地址被修改后,就会将自己工作内存中的缓存行设置成无须状态,当CPU对此变量进行修改时会重新从系统主内存中读取变量。
Volatile的有序性(禁止指令重排)
一般来说,我们写程序的时候,都是要把先代码从上往下写,默认的认为程序是自顶向下顺序执行的,但是CPU为了提高效率,在保证最终结果准确的情况下,是会对指令进行重新排序的。就是说写在前的代码不一定先执行,在后面的也不一定晚执行。
举个例子:
int a = 5; // 代码1 int b = 8; // 代码2 a = a + 4; // 代码3 int c = a + b; // 代码4