这里还得提一个概念,as-if-serial
。
as-if-serial
不管怎么重排序,单线程下的执行结果不能被改变。
编译器、runtime和处理器都必须遵守as-if-serial语义。
那Volatile是怎么保证不会被执行重排序的呢?
内存屏障
java编译器会在生成指令系列时在适当的位置会插入内存屏障
指令来禁止特定类型的处理器重排序。
为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:
需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障。
写
读
上面的我提过重排序原则,为了提高处理速度,JVM会对代码进行编译优化,也就是指令重排序优化,并发编程下指令重排序会带来一些安全隐患:如指令重排序导致的多个线程操作之间的不可见性。
如果让程序员再去了解这些底层的实现以及具体规则,那么程序员的负担就太重了,严重影响了并发编程的效率。
从JDK5开始,提出了happens-before
的概念,通过这个概念来阐述操作之间的内存可见性。
happens-before
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
如果现在我的变了falg变成了false,那么后面的那个操作,一定要知道我变了。
聊了这么多,我们要知道Volatile是没办法保证原子性的,一定要保证原子性,可以使用其他方法。
无法保证原子性
就是一次操作,要么完全成功,要么完全失败。
假设现在有N个线程对同一个变量进行累加也是没办法保证结果是对的,因为读写这个过程并不是原子性的。
要解决也简单,要么用原子类,比如AtomicInteger,要么加锁(记得关注Atomic的底层
)。
应用
单例有8种写法,我说一下里面比较特殊的一种,涉及Volatile的。
大家可能好奇为啥要双重检查?如果不用Volatile会怎么样?
我先讲一下禁止指令重排序
的好处。
对象实际上创建对象要进过如下几个步骤:
上面我不是说了嘛,是可能发生指令重排序的,那有可能构造函数在对象初始化完成前就赋值完成了,在内存里面开辟了一片存储区域后直接返回内存的引用,这个时候还没真正的初始化完对象。
但是别的线程去判断instance!=null,直接拿去用了,其实这个对象是个半成品,那就有空指针异常了。
可见性怎么保证的?
因为可见性,线程A在自己的内存初始化了对象,还没来得及写回主内存,B线程也这么做了,那就创建了多个对象,不是真正意义上的单例了。
上面提到了volatile与synchronized,那我聊一下他们的区别。
volatile与synchronized的区别
volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。volatile用于禁止指令重排序:可以解决单例双重检查对象初始化代码执行乱序问题。
volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。
总结
注:以上所有的内容如果能全部掌握我想Volatile在面试官那是很加分了,但是我还没讲到很多关于计算机内存那一块的底层,那大家就需要后面去补课了,如果等得及,也可以等到我写计算机基础章节。
絮叨
因为更新文章和视频,丙丙已经半年多的周末没休息了,都是在公司那个工位冲冲冲,一直想找时间出去玩,想着年假一天没用,就请了两天出去玩一下。
这样五一就可以早点回来,准备恢复视频的更新,你在看的时候呢,敖丙应该在出游的列车上了,是的我就背了这个包,到写完的时候,我还没确定去哪里,提前祝大家节日愉快。
我是敖丙,一个在互联网苟且偷生的工具人。
你知道的越多,你不知道的越多,人才们的 【三连】 就是丙丙创作的最大动力,我们下期见!
注:如果本篇博客有任何错误和建议,欢迎人才们留言,你快说句话啊!