volatile的特性:
1、不保证原子性
2、保证多线程下的资源可见性
3、禁止指令重排
4、只能修饰 全局变量或者静态全局变量
下面展开介绍上面的四大特性
保证多线程下的资源可见性:
首先,要知道 为什么多线程下为什么会出现资源的不可见
简单说一下场景就是:在多线程并发执行下,多个线程修改共享的成员变量,会出现一个线程修改了共享变量的值后,另一个线程不能直接看到该线程修改后的变量的最新值。
首先需要了解一下 虚拟机的 java 内存模型
下面讲一个实例
上图 定义了一个线程,该线程的任务是,休眠了一秒之后,修改了flag 的标志。而主线程要做的是拿到该对象的 flag值来输出一段话,会出现的结果就是,死循环永远无法拿到flag 为true 的状态,而无法输出打印语句:主线程进入循环执行~~~~
为什么会这样呢?
根据jmm,java内存模型的条件可知,所有的共享变量都会放到主内存当中,而线程所需要的变量会从主内存复制一个副本到自己的工作内存当中,这样就会导致,子线程和主线程读到的flag都是初始值false没有问题,但是在子线程已经完成修改之后,由jmm还可知,线程首先修改的是自己的工作内存,然后才会同步到主内存当中,而此时的主线程的工作内存并没有更新,读取的还是 flag=false(这里主线程没有更新工作内存的原因是:jmm会有一个优化机制,就是当使用了工作内存中的一个值多次之后,发现主内存该值一直没有变化,那么就不会再去主内存查看该值是否需要更新了)。
多线程下共享变量的可见性问题如何解决
1、加锁 synchronized:
2、对共享的变量使用volatile来修饰
1、加锁
原因:
这里的解释很清晰,就是每次进入到synchronized的代码块之后,该线程下的工作内存都会被清空,而被清空后就不得不从主内存获取最新的值最为工作内存,此时就完成了可见性问题。
2、使用volatile修饰共享变量
原因:
上图对可见性的解释很清晰,就是说某一个线程修改了副本当中的公共变量之后,会对主内存的值进行修改,同时会让其他线程的副本的改值失效,而不得不重新从主线程中获取最新值。
不保证原子性:
问题分析
即使是使用了volatile来修饰公共变量也没办法改变上述结果
原子性的问题解决
1、加锁
2、使用原子类
指令重排
示例
a、b、i、j的默认值都为0,分别执行上述两个线程,并打印出i和j的值
上述的结果都是线程并发正常可能会出现的
但是什么情况下会出现 i和j为0的情况,是因为编译器出现了指令重排的现象,就是颠倒了线程任务当中原有的顺序,变成了i=b;a=1;和j=a;b=1,此时就会出现i=0和j=0.
问题解决
把四个值都使用volatile 来修饰,实现了禁止指令重排序以及其带来的安全性的问题。
volatile内存语义
示例
如上图所示,公共变量b 被volatile修饰了,那么此时当完成 b=a的操作后,那么a=3和abc=100都是可见的。
结论
volatile的使用情景
1、纯赋值操作
在这里插入图片描述
因为赋值自身是携带原子性的,所以弥补了volatile的不足,因为可以满足线程安全。
2、触发器
这个特性利用了上述讲到的 happen-before 的原则
主线程来执行两个线程的读写,结论就是如果 flag为true,那么写的操作就都实现了 a=100,b=2–,c=300,
volatile和synchronized的区别
双重检查机制 实现懒汉式单例
上图中 为什么需要给 单例的声明变量增加volatile 的修饰呢?
正常执行顺序如下
但是由于底层可能会出现重排序导致a、b、c三个步骤顺序出现问题
就是说如果INSTANCE没有使用volatile修饰,那么可能会出现a、c、b的现象,此时引用中有数据,但是并没有初始化实例,而图中的线程c经过判断返现引用不是null,则返回一个空对象出去,将可能在后续调用该对象的属性时,出现对象空指针问题。