Volatile
特性:保证可见性,防止指令重排,不保证原子性
JMM
三大特性:可见性、原子性、有序性
八大原子操作
read读取:将主内存中的变量加载到工作内存
load加载:将工作内存中的数据赋给工作内存中的变量
use操作:将赋值后的变量进行计算操作
assign赋值:将计算后的数据赋给工作内存中的变量
store存储:将赋值后的数据写回主内存
write写入:将上一步主内存中的数据赋值给对应的变量
lock加锁:将变量标记为线程独占状态
unlock解锁:将加锁的标记解除
lock和unlock介入时机
被volatile修饰的对象在store之前会对主内存中变量加锁lock,加锁后其他线程将无法对主内存对应的变量进行操作,当持有锁执行完store和write后,会释放锁unlock。
因为lock--unlock之间就是单纯的内存操作,所以速度非常快,几乎不影响性能。
为什么可以保证可见性( MESI缓存一致性协议)
例如:
1. 定义一个变量 public volatile int i=0;
2. A、B、C线程同时将i加载到工作内存中
3. A线程完成了load操作;B线程完成了assign操作(i+1);C线程完成了store操作
4. 当C线程完成store操作后,因为变量i是被volatile修饰的,所以会通过MESI协议分别通知 A线程和B线程,将AB线程中工作内存中的变量i进行失效(我理解为将i=null)
5. 这时AB线程再继续操作的时候发现i=null了,就要去主内存中获取(read)新的值,这样 就保证了可见性
其他相关信息:
-
在第五步,read时线程C还没完成unlock操作,那么线程会阻塞等待;
-
如果没有被volatile修饰,在执行完assign操作后,线程会继续执行后面业务的操 作,不确定什么时候才会将i变量调用storewrite写回主内存;如果被volatile修饰 了,即便后面还有很多业务流程,线程也不会去执行,线程会立刻执行storewrite 操作
为什么不保证原子性
在上面的例子中
- 初始状态下i=0
- ABC线程同时将i=0加载到工作内存中,而B线程完成了use操作(i+1),并且将1写回了工作内存中,这时B线程工作内存变量i值为1;
- C线程完成了store操作,并且完成write操作,此时主内存中变量i值由0变为1;
并且通过MESI协议使AB线程工作内存中的变量i失效;
- 这时,线程B要执行store操作了,发现工作内存中的i失效了,便重新从主内存中读取一份,然后再执行store操作,但这时读取i的值为1,这个1是线程C计算得出的,而不是线程B计算得出的,丢失了B线程操作的记录
以上,就是为什么volatile不保证原子性原因
如何避免不保证原子性
- synchronized
- lock
- Atomic原子类
防止指令重排
例如:new 一个User对象 User user=new User();
通过javap命令得到字节码文件可得知,new操作分为三步
- 在栈上创建内存
- 在堆中初始化对象
- 将栈和堆进行引用
如果user没有volatile修饰,那么执行流程可能是123,132,312。。。。
加上了修饰,只会是123.
这个在单例模式中体现最为明显,如果不加修饰,可能会导致有引用,但为初始化,导致报错
八个原子操作流程图
read->load->use->assign->lock->store->write->unlock
虚线嗅探机制为实时监听中。
红色线表示不保证volatile原子性的阶段