那么当写两条线程Thread-A与Threab-B同时操做主存中的一个volatile变量i时,Thread-A写了变量i,那么:java
Thread-A发出LOCK#指令编程
Thread-B读取变量i,那么:缓存
由此能够看出,volatile关键字的读和普通变量的读取相比基本没差异,差异主要仍是在变量的写操做上。安全
为何static volatile int i = 0; i++;不保证线程安全?多线程
由于i++并非一个原子操做(这是由i++自己特质决定的),它包含了三步(实际上对应的机器码步骤更多,可是这里分解为三步已经足够说明问题):架构
一、获取i
二、i自增
三、回写i并发
A、B两个线程同时自增i
因为volatile可见性,所以步骤1两条线程必定拿到的是最新的i,也就是相同的i
可是从第2步开始就有问题了,有可能出现的场景是线程A自增了i并回写,可是线程B此时已经拿到了i,不会再去拿线程A回写的i,所以对原值进行了一次自增并回写
这就致使了线程非安全,也就是你说的多线程技术器结果不对jvm
若是线程A对i进行自增了之后cpu缓存不是应该通知其余缓存,而且从新load i么?高并发
拿的前提是读,问题是,线程A对i进行了自增,线程B已经拿到了i并不存在须要再次读取i的场景,固然是不会从新load i这个值的。性能
ps:也就是线程B的缓存行内容的确会失效。可是此时线程B中i的值已经运行在加法指令中,不存在须要再次从缓存行读取i的场景。
volatile是“轻量级”synchronized,保证了共享变量的“可见性”(JMM确保全部线程看到这个变量的值是一致的),当CPU写数据时,若是发现操做的变量是共享变量,即在其余CPU中也存在该变量的副本,会发出信号通知其余CPU将该变量的缓存行置为无效状态而且锁住缓存行,所以当其余CPU须要读取这个变量时,要等锁释放,并发现本身缓存行是无效的,那么它就会从内存从新读取。
volatile是“轻量级”synchronized,保证了共享变量的“可见性”(JMM确保全部线程看到这个变量的值是一致的),使用和执行成本比synchronized低,由于它不会引发线程上下文切换和调度。
工做内存Work Memory其实就是对CPU寄存器和高速缓存的抽象,或者说每一个线程的工做内存也能够简单理解为CPU寄存器和高速缓存。
volatile做用:
1.锁总线,其它CPU对内存的读写请求都会被阻塞,直到锁释放,不过实际后来的处理器都采用锁缓存替代锁总线,由于锁总线的开销比较大,锁总线期间其余CPU无法访问内存
2.lock后的写操做会回写已修改的数据,同时让其它CPU相关缓存行失效,从而从新从主存中加载最新的数据
3.不是内存屏障却能完成相似内存屏障的功能,阻止屏障两遍的指令重排序
volatile只能保证对单次读/写的原子性。由于long和double两种数据类型的操做可分为高32位和低32位两部分,所以普通的long或double类型读/写可能不是原子的。所以,鼓励你们将共享的long和double变量设置为volatile类型,这样能保证任何状况下对long和double的单次读/写操做都具备原子性。
队列集合类LinkedTransferQueue,在使用volatile变量时,追加64字节的方式来优化队列出队和入队的性能。
追加字节能优化性能?这种方式看起来很神奇,但若是深刻理解处理器架构就能理解其中的奥秘。让咱们先来看看LinkedTransferQueue这个类,它使用一个内部类类型来定义队列的头节点(head)和尾节点(tail),而这个内部类PaddedAtomicReference相对于父类AtomicReference只作了一件事情,就是将共享变量追加到64字节。咱们能够来计算下,一个对象的引用占4个字节,它追加了15个变量(共占60个字节),再加上父类的value变量,一共64个字节。
为何追加64字节可以提升并发编程的效率呢?由于对于英特尔酷睿i七、酷睿、Atom和NetBurst,以及Core Solo和Pentium M处理器的L一、L2或L3缓存的高速缓存行是64个字节宽,不支持部分填充缓存行(处理器支持也能够),这意味着,若是队列的头节点和尾节点都不足64字节的话,处理器会将它们都读到同一个高速缓存行中,在多处理器下每一个处理器都会缓存一样的头、尾节点,当一个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性机制的做用下,会致使其余处理器不能访问本身高速缓存中的尾节点,而队列的入队和出队操做则须要不停修改头节点和尾节点,所以在多处理器的状况下将会严重影响到队列的入队和出队效率。
Doug lea使用追加到64字节的方式来填满高速缓冲区的缓存行,避免头节点和尾节点加载到同一个缓存行,使头、尾节点在修改时不会互相锁定。
那么是否是在使用volatile变量时都应该追加到64字节呢?不是的。在两种场景下不该该使用这种方式。
缓存行非64字节宽的处理器。如P6系列和奔腾处理器,它们的L1和L2高速缓存行是32个字节宽。
共享变量不会被频繁地写。由于使用追加字节的方式须要处理器读取更多的字节到高速缓冲区,这自己就会带来必定的性能消耗,若是共享变量不被频繁写的话,锁的概率也很是小,就不必经过追加字节的方式来避免相互锁定。
volatile关键字使用的是Lock指令,volatile的做用取决于Lock指令。CAS不是保证原子的更新,而是使用死循环保证更新成功时候只有一个线程更新,不包括主工做内存的同步。 CAS配合volatile既保证了只有一个线程更新又保证了多个线程更新得到的是最新的值互不影响。
volatile的变量在进行写操做时,会在前面加上lock质量前缀。
Lock前缀,Lock不是一种内存屏障,可是它能完成相似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,能够理解为CPU指令级的一种锁。
而Lock前缀是这样实现的
它先对总线/缓存加锁,而后执行后面的指令,最后释放锁后会把高速缓存中的脏数据所有刷新回主内存。
在Lock锁住总线的时候,其余CPU的读写请求都会被阻塞,直到锁释放。Lock后的写操做会让其余CPU相关的cache失效,从而重新从内存加载最新的数据,这个是经过缓存一致性协议作的。
lock前缀指令至关于一个内存屏障(也称内存栅栏)(既不是Lock中使用了内存屏障,也不是内存屏障使用了Lock指令),内存屏障主要提供3个功能:
内存屏障是CPU指令。若是你的字段是volatile,Java内存模型将在写操做后插入一个写屏障指令,在读操做前插入一个读屏障指令。
下面是基于保守策略的JMM内存屏障插入策略:
在每一个volatile写操做的前面插入一个StoreStore屏障。
在每一个volatile写操做的后面插入一个StoreLoad屏障。
在每一个volatile读操做的前面插入一个LoadLoad屏障。
在每一个volatile读操做的后面插入一个LoadStore屏障。
内存屏障,又称内存栅栏,是一组处理器指令,用于实现对内存操做的顺序限制。
内存屏障能够被分为如下几种类型
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操做要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操做执行前,保证Store1的写入操做对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操做被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续全部读取操做执行前,保证Store1的写入对全部处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
StoreLoad Barriers是一个“全能型”的屏障,它同时具备其余3个屏障的效果。
在每一个volatile写操做前插入StoreStore屏障(这个屏障先后的2个Store指令不能交换顺序),在写操做后插入StoreLoad屏障(这个屏障先后的2个Store Load指令不能交换顺序);
在每一个volatile读操做前插入LoadLoad屏障(这个屏障先后的2个Load指令不能交换顺序),在读操做后插入LoadStore屏障(这个屏障先后的2个Load Store指令不能交换顺序);
Java经过几种原子操做完成工做内存和主内存的交互:
lock:做用于主内存,锁住主内存主变量。
unlock:做用于主内存,解锁主内存主变量。
read:做用主内存,主内存传递到工做内存。
load:做用于工做内存,主内存传递来的值赋给工做内存工做变量。
use:做用工做内存,工做内存工做变量值传给执行引擎。
assign:做用工做内存,引擎的结果值赋值给工做内存工做变量。
store:做用于工做内存的变量,工做内存工做变量传送到主内存中。
write:做用于主内存的变量,工做内存传来工做变量赋值给主内存主变量。‘
’
read and load 从主存复制变量到当前工做内存
use and assign 执行代码,改变共享变量值
store and write 用工做内存数据刷新主存相关内容
其中use and assign 能够屡次出现
可是这一些操做并非原子性,也就是在read load以后,若是主内存count变量发生修改以后,线程工做内存中的值因为已经加载,不会产生对应的变化,因此计算出来的结果会和预期不同.