概念
JMM规范解决了线程安全的问题,主要三个方面:原子性、可见性、有序性,借助于synchronized关键字体现,能够有效地保障线程安全(前提是你正确运用)
以前说过,这三个特性并不必定须要所有同时达到,在有些场景,部分达成也可以作到线程安全。
volatile就是这样一个存在,对可见性和有序性进行保障
可见性
volatile字面意思,易变的,不稳定的,在Java中含义也是如此
想要保证可见性,就要保障一个线程对于数据的操做,可以及时的对其余线程可见
volatile会通知底层,指示这个变量读取时,不要经过本地缓存,而是直接去主存中读取(或者说本地内存失效,必须去主存读取),这样若是一个线程对于数据完成写入到主存,另外线程进行读取时,就能够第一时间读取到新值,而非旧值,因此所谓不稳定,就是指可能会被其余线程同时并发修改,因此你要去主存中去从新读取。
他会让写线程冲刷写缓存,读线程刷新读缓存,简言之就是操做后马上会刷新数据,读取前也会刷新数据;
以保证最新值能够及时更新到主存以及读线程及时的读取到最新值。
注意:
若是Reader对于这个共享变量x的读取操做有不少个步骤,好比x=1;y=x;y=y+1;y=y+2;等等 最后x=y;,若是没有原子性保障,很显然,若是已经执行过了y=x;再日后的操做过程当中,若是x的值再次被改变了,此时Reader中的y是没法改变的,这就出现问题了
因此此处的可见性要注意区分,在某些场景想要线程安全的话,可见性对原子性是有依赖的
可见性指的是在你须要的时刻,若是被别人修改了,从新读取新的,可是若是你用过了,单纯的可见性并不能保证后续没问题。
有序性
volatile关键字将会直接禁止JVM和处理器对关键字修饰的指令重排序,可是对于volatile关键字修饰的先后的、无依赖的指令,能够进行重排序
被volatile修饰的变量,能够认为插入了一个内存屏障,他会进行以下保障:
- 确保指令重排序时不会将其后面的代码排到内存屏障以前
- 确保指令重排序时不会将其前面的代码排到内存屏障以后
- 确保在执行到内存屏障修饰的指令时前面的代码所有执行完成
- 强制将线程工做内存中值的修改刷新至主内存中
- 若是是写操做,则会致使其余线程工做内存(CPU Cache)中的缓存数据失效
好比
int x = 0;
int y = 1;
volatile int z=20;
x++;
y--;
在语句volatile int z=20以前,先执行x的定义仍是先执行y的定义,咱们并不关心,只要可以百分之百地保证在执行到z=20的时候x=0, y=1,同理关于x的自增以及y的自减操做都必须在z=20之后才能发生。这个结果就是上面的逻辑处理后的结果。
综上所述,volatile能够对可见性以及有序性进行保障。
那么volatile的原子性如何?
原子性
以下面示例,共享变量count是volatile的,在add方法中,对他进行自增,运行几回后分别查看结果
package test1;
public class T12 {
public static volatile int count = 0;
public static void add() {
count++;
}
public static void main(String[] args) {
//建立10个线程,每一个线程循环1000次,最终结果应该是10,000
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
// 确认其余线程都结束了,不然不继续执行(确认当前线程组以及子线程组活动线程的个数,JDK8中这个值设置为2),后续有更好的方法完成等待
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("count: " + count);
}
}
10个线程,每一个线程1000次循环,按理来讲最终的结果应该是1000
从结果能够看得出来,并非线程安全的,可是既然volatile保障了可见性与有序性,能够推断出来并无作到原子性
问题出在哪里?
关键在于count++;自增操做,并非直接的赋值操做,好比x=1;
他能够简单的理解为三个步骤:
- 读取count的值;
- 操做count的值;
- 回写count的值;
volatile能够保障在第一步的时候,读取到了正确的值,可是因为不是原子的,在接下来的操做过程当中,count的值,可能已经被更新过了,也就是读取到了旧值
继续使用这个旧值很显然就把别人的更新抹掉了,你读取的1,可能此时应该是2了,可是你操做后仍是2,无端的擦除了别人的增长,因此结果才会出现小于10000的状况
由于是自增操做,因此使用旧值会致使小于10000
若是把初始值设置为10000,使用自减count--,使用旧值就可能会致使别人的减量被擦除了,最终大于0,不妨修改成自减运算试一下
从结果看得出来,咱们的推断没错,就是使用了旧值
这就是前面说到的线程安全,单纯的依赖可见性是不能保障的,还须要依赖原子性
由于在第一步的时候,尽管获取到的值确定是最新的,可是接下来的过程当中呢?
值仍旧可能被改变,由于并非原子的
好比,装着饮料的瓶子,你从其中取饮料
可见性能够保障你要倒饮料的时候,瓶子里面是可乐你到出来的是可乐,装的是雪碧,倒出来就是雪碧,可是若是你把可乐倒进本身的杯子里面了,瓶子瞬间换成雪碧,你杯子里面的可乐会变化吗?
回想下以前设计模式中介绍过的单例模式,有一种实现方式是双重检查法
public class LazySingleton {
private LazySingleton() {
}
private static volatile LazySingleton singleton = null;
public static LazySingleton getInstance() {
if (singleton == null) {
synchronized (LazySingleton.class) {
if (singleton == null) {
singleton = new LazySingleton();
}
}
}
return singleton;
}
}
注意:
private static volatile LazySingleton singleton = null;html
使用volatile修饰
由于实例建立语句:singleton = new LazySingleton(); ,就不是一个原子操做
他可能须要下面三个步骤
- 分配对象须要的内存空间
- 将singleton指向分配的内存空间
- 调用构造函数来初始化对象
计算机为了提升执行效率,会作的一些优化,在不影响最终结果的状况下,可能会对一些语句的执行顺序进行调整
也就是上面三个步骤的顺序是不可以保证惟一的
若是先分配对象须要的内存,而后将singleton指向分配的内存空间,最后调用构造方法初始化的话
假如当singleton指向分配的内存空间后,此时被另外线程抢占(因为不是原子操做因此可能被中间抢占)
线程2此时执行到第一个if (singleton == null)
此时不为空,那么不须要等待线程1结束,直接返回singleton了
显然,此时的singleton都尚未彻底初始化,就被拿出去使用了
根本问题就在于写操做未结束,就进行了读操做
重排序致使了线程的安全问题
此时能够给 singleton 的声明加上volatile关键字,以保障有序性
上面的两个示例,看起来都是没有保障原子性,可是为何一个使用volatile修饰就能够,而另一个则不行?
对于count++,运算结果的正确性依赖count当前的值自己,并且可能存在多个线程对他进行修改,而singleton则不依赖,并且也不会多个线程进行修改
因此说,volatile的使用要看具体的场景,这也是为何被称之为轻量级的synchronized的缘由,他不能从原子性、可见性、有序性三个角度进行保障。
因此从上面这些点也能够看得出来,volatile并不能替代synchronized,很关键的一个点就是他并不能保障原子性
volatile与synchronized对比
总结
volatile是一种轻量级的同步方式(轻量级的synchronized,也就是阉割版的synchronized)
抛开性能的角度看,synchronized的正确使用能够百分百解决同步问题,可是volatile却并不能彻底解决同步问题,由于他缺少一个很重要的保障---原子性
原子性可以保障不可分割,一旦不能对原子性进行保障,一旦一个变量的修改依赖自身,好比i++,也就是i=i+1;依赖自身的值,一旦再多线程环境中,仍旧可能会出错
因此若是换一个思路理解的话,能够这样:
对于线程安全问题,主要是三个方面,原子性、可见性、有序性,不过并不必定全部的场景都须要三者彻底保障;
对于synchronized关键字都进行了保障,能够用于线程安全的同步问题
对于volatile,他对可见性和有序性进行了保障,因此若是在有些场景下,若是仅仅保障了这二者就能够达到线程安全,那么volatile也能够用于线程的同步
因此说synchronized能够用于同步,volatile能够用于部分场景的线程同步
刚才提到对于i++,仅仅借助于volatile,他至关于i=i+1,依赖自身的值的内容,因此多线程会出问题,若是只有一个线程才会执行这个操做就不会出现问题
另外,若是对于一个操做,好比i=j+1;j也是一个共享变量,很显然多线程场景下,仍旧可能出现问题
因此若是你使用volatile保障线程安全,须要很是慎重,必要的时候,仍旧须要借助于synchronized关键字进行同步,进一步对原子性进行保障。