缓存不一致性问题:在多核CPU中,每条线程可能运行于不一样的CPU中,所以每一个线程运行时有本身的高速缓存(对单核CPU来讲,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。c++
解决方法:缓存
CPU和其余部件进行通讯都是经过总线来进行的,对总线加LOCK#锁,阻塞了其余CPU对其余部件访问(如内存),只能有一个CPU能使用这个变量的内存。并发
最出名的就是Intel的MESI协议,MESI协议保证了每一个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,若是发现操做的变量是共享变量,即在其余CPU中也存在该变量的副本,会发出信号通知其余CPU将该变量的缓存行置为无效状态,所以当其余CPU须要读取这个变量时,发现本身缓存中缓存该变量的缓存行是无效的,那么它就会从内存从新读取。优化
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰以后,那么就具有了两层语义:spa
1)保证了不一样线程对这个变量进行操做时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。线程
2)禁止进行指令重排序(他上面的比他先执行,他下面的比他后执行,但并不保证上面和下面的代码块也是有序执行)code
保证可见性:对象
volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。并且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任什么时候刻,两个不一样的线程老是看到某个成员变量的同一个值。排序
不保证原子性:内存
譬如inc++,自增操做是不具有原子性的,它包括读取变量的原始值、进行加1操做、写入工做内存。那么就是说自增操做的三个子操做可能会分割开执行,就有可能致使下面这种状况出现:
假如某个时刻变量inc的值为10,
线程1对变量进行自增操做,线程1先读取了变量inc的原始值,而后线程1被阻塞了;
而后线程2对变量进行自增操做,线程2也去读取变量inc的原始值,因为线程1只是对变量inc进行读取操做,而没有对变量进行修改操做,因此不会致使线程2的工做内存中缓存变量inc的缓存行无效,因此线程2会直接去主存读取inc的值,发现inc的值时10,而后进行加1操做,并把11写入工做内存,最后写入主存。
而后线程1接着进行加1操做,因为已经读取了inc的值,注意此时在线程1的工做内存中inc的值仍然为10,因此线程1对inc进行加1操做后inc的值为11,而后将11写入工做内存,最后写入主存。
那么两个线程分别进行了一次自增操做后,inc只增长了1。
解释到这里,前面保证的是一个变量在修改volatile变量时,会让缓存行无效,而后其余线程去读就会读到新的值。可是要注意,线程1对变量进行读取操做以后被阻塞了,并无对inc值进行修改。而后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,可是线程1没有进行修改,因此线程2根本就不会看到修改的值。根源就在这里,自增操做不是原子性操做,并且volatile也没法保证对变量的任何操做都是原子性的。
必定程度上保证有序性:
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操做或者写操做时,在其前面的操做的更改确定所有已经进行,且结果已经对后面的操做可见;在其后面的操做确定尚未进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
JVM角度:
Java使用一个主内存来保存变量当前值,而每一个线程则有其独立的工做内存。
线程访问变量的时候会将变量的值拷贝到本身的工做内存中,这样,当线程对本身工做内存中的变量进行操做以后,就形成了工做内存中的变量拷贝的值与主内存中的变量值不一样。
Java语言规范中指出:为了得到最佳速度,容许线程保存共享成员变量的私有拷贝,并且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。
这样当多个线程同时与某个对象交互时,就必需要注意到要让线程及时的获得共享成员变量的变化。
而volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,会使在工做内存中的私有拷贝失效,从而再去主内存取值。
汇编角度:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上至关于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障以前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操做已经所有完成;
2)它会强制将对缓存的修改操做当即写入主存;
3)若是是写操做,它会致使其余CPU中对应的L1&L2缓存行无效。
使用条件:
一般来讲,使用volatile必须具有如下2个条件:
1)对变量的写操做不依赖于当前值
2)该变量没有包含在具备其余变量的不变式中
实际上,这些条件代表,能够被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,个人理解就是上面的2个条件须要保证操做是原子性操做,才能保证使用volatile关键字的程序在并发时可以正确执行。
下面列举几个Java中使用volatile的几个场景。
1.状态标记量
volatile
boolean
flag =
false
;
while
(!flag){
doSomething();
}
public
void
setFlag() {
flag =
true
;
}
volatile
boolean
inited =
false
;
//线程1:
context = loadContext();
inited =
true
;以前已被初始化,用到了必定程度的有序性
inited =
true
;
//线程2:
while
(!inited ){
sleep()
}
doSomethingwithconfig(context);