在了解volatile的原理前,我们先来看个示例代码:
public class Visualable { public static boolean initFlag=false; public static void main(String[] args) throws InterruptedException { new Thread(()->{ System.out.println("线程1开始执行"); while (!initFlag){ //nothing todo } System.out.println("线程执行1执行完毕"); }).start(); TimeUnit.SECONDS.sleep(2); new Thread(()->{ System.out.println("线程执行2开始执行"); initFlag=true; System.out.println("线程执行2执行完毕"); }).start(); } }
执行后显示:System.out.println("线程执行1执行完毕"); 这行代码没执行
说明了线程2修改initFlag的值并没有让线程1感知到;为何?
这里我们就要了解Java线程内存模型了,在硬件级别来看:
上图所示:一开始线程1和线程2都从主内存中将initFlag的值,读回了线程自己的内存中,此时线程2将值改了,但还没及时刷回主内存中,所以线程1感觉不到值的改变,因此线程1一直死循环,只要写
回了主内存,线程1就可以感觉到数据的改变,这个是由MSEI缓存一致性协议决定的,由各个cpu厂商实现,简单点说,每个线程都会监听总线,数据的变更要经过总线,然后,线程监听到变量变更后,会检查
自己的内存有没该变量,有就失效自己的变量,之后从主内存中读取,既然MSEI能保证内存的可见性,为何还会有问题,这是因为线程2没有及时将变量刷回主内存,而volatile可以保证变量的修改立即刷回主内存,因此保证可见性,因此代码修改一下:
那为何volatile没法保证变量的原子性: 我们思考一个问题,如果一个变量 int sum=0;线程1和线程2都读取到内存中,然后都做了++操作,那么线程1如果先刷回内存中,线程2的内存变量就失效了,此时sum的值值加了1
那volatile的禁止指令重排又是啥?先看个demo
为何会出现a=1 和b=1同时出现的情况,正常来说,a要等于1 证明y=1这行代码已经执行了,因为a=y,然而y=1执行了,证明b=x要先运行,那么b应该等于0才对,因为x此时等于0
出现这个结果的原因: 线程one中,a=y和x=1的代码顺序调换了,也就是指令重排了,线程Two b=x 和 y=1的代码顺序也重排了
什么时候会重排:遵循下面2个原则:
as-if-serial 语义:简单的说就是:能否重排,最重要的是,对于单线程来说,重排前后,结果不变,对于上面例子,对于线程one来说 a=y和x=1的顺序调换,结果是一样的,线程Two也是一样道理
happen-before 原则:
1. 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
2. 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
3. volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操作
4. happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
5. 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
6. 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
7. 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
8. 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。
这些规则来描述了什么时候可以重排
如何解决指令重排,很简单,只要在变量中加入volatile 关键字即可;
volatile 禁止重排原理:内存屏障,举个例子:
int a=1;
int b=2;
我们要系统禁止这2行代码进行重排,我们需要定义一个规范,就像产品经理说,我要实现某个功能,这是一种规范,真正实现由程序员去弄,现在假设我们的规范是,只要这两行代码中有abc三个字母就禁止重排
int a=1;
abc;
int b=2;
然后我们系统发现有abc就不会重排了,具体实现这个需求,是由jvm厂商去实现,如hotsport虚拟机:
内存屏障中的abc字母在hostport中有4种情况,读读屏障(对应的字母是 loadload),写写屏障(storestore),读写屏障(loadstore),写读屏障(storeload),汇编源码:
指令重排在单例模式中的运用:
上面经典的实现单例的双空判断,这个会有问题么?阿里规范也建议不要写成上面这种模式:
假设a线程已经执行到 instance=new MyInstance()这一步了:
在底层,这一步不是原子操作,它分为:先开辟空间-》执行init方法,初始化成员变量,假如有个int a=9的成员变量,当a线程刚执行到init方法去初始化a=9;此时线程b过来了,它发现instance不为空了,那么线程b拿到的instance是一个半成品,此时使用其中
的成员变量就会有问题