volatile通常被比喻为“轻量级的synchronized”,它是Java并发编程中的比较重要的一个关键字。和synchronized不同的是,volatile它只是一个变量修饰符,只能用来修饰变量,无法修饰方法及代码块。
volatile的应用
volatile的用法很简单,只需要在声明一个可能被多线程同时访问的变量时,使用volatile修饰就可以了。
1 | public class Singleton{ |
在双重锁检查中volatile关键词保证了在初始化对象时不会因指令重排造成问题,当还没完成构造时,导致其他线程直接获得未构造的对象。在初始化对象时的顺序为:1.分配内存空间;2.执行构造方法,初始化对象;3.把这个对象指向这个空间。
volatile原理
为了提高处理器的执行速度,在处理器和内存之间增加了许多缓存来提升。但是由于引入了多级缓存,就存在了缓存数据不一致问题。但是,对于volatile变量,当对volatile变量进行写操作时,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议。
缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作,会强制重新从系统内存里把数据读到处理器缓存里。
如果一个变量被volatile修饰的话,在每次数据变化之后,其值都会被强制刷入主存中。而其他处理器的缓存遵守了缓存一致性协议,也会把这个变量值从主存加载到自己的缓存中。这就保证了volatile在并发编程中,其值在多个缓存中是可见的。
volatile与可见性
可见性是指多个线程访问同一个变量时,一个线程修改了这个变量值,其他线程能够立即看到修改的值。
Java内存模型中规定了所有变量都存储在主内存中,每条线程有自己的工作内存,线程的工作内存保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主内存之间进行数据同步。所以,就可能出现了线程1改变某个变量的值,但线程2不可见的情况。
Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后立即同步到主内存中,被其修饰的变量在每次使用之间都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。
volatile与有序性
有序性即程序执行的顺序按照代码的先后顺序执行。
由于处理器和指令重排等,CPU可能对输出代码进行扰乱执行,比如load->add-save有可能被优化成load->save->add。这就是可能存在的有序性问题。
而volatile除了可以保证数据的可见性之外,还有一个强大的功能就是可以禁止指令重排优化等。
volatile与原子性
原子性是指一个操作是不可中断的,要不全部执行完成,要不就都不执行。
线程是CPU调度的基本单位。CPU会有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU的使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。
volatile不能保证原子性。
总结
- volatile可以保证变量对所有线程可见
- 写一个volatile变量时,把该线程工作内存中的值刷新到主内存中
- 读一个volatile变量时,把该线程工作内存值置为无效,从主内存中读取
- volatile禁止指令重排序优化
- 第二个操作是volatile写,不管第一个操作是什么都不能重排序,确保写之前的操作不会被重排序到写之后
- 第一个操作是volatile读,不管第二个操作是什么都不能重排序,确保读之后操作不会被重排序到度之前。
- 第一个操作是volatile写,第二个操作是volatile读不能重排序。