说到volatile就不得不说说JMM(Java Memory Model) java内存模型了,这是java虚拟机规范中所定义的一种内存模型,java内存模型是标准的,屏蔽掉了底层不同计算机的区别.
背景:
早期计算机中的cpu和内存的速度是差不多的,但后来cpu指令速度远超过内存的存取速度,由于计算机的存储设备与处理器的运行速度有几个数量级的差距,所以现代计算机系统不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存与处理器之前的缓冲.
将运算需要用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,纸样处理器就无需等待内存读写了.
基于告诉缓存的存储交互很好的解决了处理器与内存的速度矛盾,但是也带来了更高的复杂度,由此引进一个新的问题:缓存一致性(CacheCoherence)
在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)
JMM:
Java内存模型描述了java程序中各种变量(线程共享变量)的访问规则,以及再JVM中将变量,存储到内存和从内存读取变量这样的底层细节.
规定:
所有共享变量都存储在主内存(实例变量和类变量),不包括局部变量(局部变量是线程私有,不存在竞争关系).
线程有自己的工作内存,存储线程使用的变量副本,不同线程之间不能直接访问对方工作内存中的变量,交互需要通过主内存完成.
可见性的解决方案:
1.加锁
线程进出synchronized代码块前后,线程会获得锁,清空工作内存,从主内存拷贝共享变量最新的值到工作内存作为副本,执行代码,修结果刷新回主内存然后线程释放锁.
而获取不到锁的线程会阻塞等待,所以变量的值一直都是新的.
2.Volatile修饰共享变量
每个线程操作数据的时候,都是从主内存读取到工作内存,如果线程操作了数据并且写回,其他线程的工作内存中的数据副本就会失效,需要重新去主内存中读取.
volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另一个线程会立刻看到最新的值.
为了解决一致性的问题,需要各个处理器访问缓存时都遵循MSI,MESI,MOSI,Synapse,Firefly,DragonProtocol等.
MESI:
当cpu写数据时,如果发现操作的变量是共享变量,会发出信号通知其他cpu将该变量的缓存行为置为无效状态,因此当其他cpu需要读取时,会重新去主内存中读取.至于怎么发现数据时效,就要引入嗅探机制了.
嗅探机制:
每个cpu通过嗅探在总线上传播的数据来检查自己缓存中的值是否失效,当cpu发现自己缓存对应的内存地址被修改,就会将当前cpu缓存的行设置为失效状态,当cpu对这个行的数据进行修改操作时,就会重新去主内存中读取了.
总线风暴:
由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带宽达到峰值.
所以不建议大量使用volatile.
禁止指令重排序:
背景:为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序.
一般重排序可以分为以下三种
1.编译器优化的重排序.编译器在不改变单线程程序语义的前提下,可以重新安排语句执行顺序
2.执行级并行的重排序.现代处理器采用了指令级并行技术来将多条指令重叠执行
3.内存系统的重排序.由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能时在乱序执行的.
as-if-serial:
不管怎么重排序,单线程下执行结果不会改变
volatile通过内存屏障保证不会被执行重排序.
内存屏障:
java编译器会在生成指令序列时在适当的位置插入内存屏障指令来禁止特定类型的处理器重排序.
volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障。
happens-before
定义:如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系.
volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读.
注意:volatile是没办法保证原子性的.
无法保证原子性:
解决:要么使用原子类,要么加锁
volatile于synchronized的区别:
volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块.
volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);
而synchronized是一种互斥机制.volatile用于禁止指令重排序;可以解决单例双重检查对象初始化代码执行乱序问题.
volatile可以看作是轻量版的synchronized,volatile不保证原子性,但是如果对一个共享变量进行多个线程的赋值,而没有其他操作,那么就可以用volatile代替synchronized,因为赋值本身是原子性的,而volatile又保证了可见性,所以是线程安全的.