单例模式和volatile
单例模式的演化
1)饿汉式:利用static关键字,在类初始化的时候就会调用静态方法
public class Singleton { private static final Singleton singleton=new Singleton(); private Singleton(){ } public static Singleton getInstance(){ return singleton; } }
缺点:这个时候可能还没使用这个对象,浪费资源 (参考:类的初始化时机)
2)单例模式优化
懒汉式:声明为null,用到的时候 在初始化,但是需要加锁防止线程并发的时候,产生两个对象
(对象持有自己成员变量,本身外面的大对象会被创建多次,这些大对象的成员变量共享)
public class Singleton { private static Singleton singleton=null; private Singleton(){ } public synchronized static Singleton getInstance(){ System.out.println("我的其他业务"); if (singleton==null){ singleton=new Singleton(); } return singleton; } }
缺点:方法中有一些不需要上锁的业务代码也给锁上了,锁的粒度在粗了
3)深度优化(把锁细化)
细化锁的时候需要两次if判断
public class Singleton { private static Singleton singleton=null; private Singleton(){ } public static Singleton getInstance(){ System.out.println("我的其他业务"); if (singleton==null){ synchronized (Singleton.class){//Double Check Lock if (singleton==null){ singleton=new Singleton(); } } } return singleton; } }
之所以DCL加两次IF: 防止多个线程同时执行到了加锁的代码
4)单例最终版本:CPU指令重排导致的安全性
因为CPU可能指令重排:声明变量的时候 需要加上volatile关键字
private static volatile Singleton singleton=null;
下面解释 为什么要加volatile关键字(先说结论:volatile可以禁止指令重排序)
在我们的idea中 idea-view-Show Bytecode可以看到我们方法的字节码
比如 Object o = new Object();
字节码
0 new #4 <java/lang/Object> 3 dup 4 invokespecial #1 <java/lang/Object.<init>> 7 astore_1 8 return
当我们new一个对象的时候,基本上分三步指令(单条指令都有可能不是原子性)
1)在堆内存申请一个对象 分配一块内存,此时对象里面的值是一个默认值,
2)然后调用构造方法 初始化,
3)把堆内存的引用赋值给o o来执行堆内存中的Object对象,建立关联
分配内存—初始化—建立关联
当一个线性1new的时候,走到第一步 分配内存时,发生了CPU的指令重排序,先建立关联(此时关联的对象 值都是空的 因为还没经过初始化),
这是线程2进来,发现对象不为空(因为第三步已经建立关联已经) 直接拿去用了 此时用的对象是一个半成品(因为线程1还没有进行初始化)
解释volatile关键字
而volatile的作用有两点
1)多线程之间的可见性(类似CPU缓存一致性协议 保持缓存行里的数据一致性)
当一个变量定义为volatile之后,此变量对所有线程可见,这里的"可见性"是指当一条线程修改了这个变量的值,新值对其他线程来说是立即得知的。
而普通变量不能做到这一点,普通变量的值在线程之间传递需要通过主内存来完成,列如,当线程A修改一个普通变量的值,然后从工作内存(类比CPU的高速缓存)向主内存进行回写,
另一个线程B在线程A回写完成了之后再从主内存(类比物理硬件的主内存)进行读取操作,新变量的值才会对线程B可见。
普通变量和volatile变量的区别是。volatile的特殊规则(主内存和工作内存之间具体的交互协议)保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。
因此,可以说volatile保证了多线程操作时变量的可见性,而普通线程不能保证这一点
关于volatile变量的可见性,经常会被误解,"volatile变量对所有线程是立即可见的,换句话说 volatile变量在各个线程中是一致的,所以基于volatile变量的运算在并发下是安全的"
加粗字体是错误的,比如i++ 这行代码转成字节码有如下指令,i++的操作过程需要多个指令,所以volatile不能保证原子性,即不能保证线程安全
0 aload_0 1 dup 2 getfield #3 //从主内存取值 5 iconst_1 6 iadd //加1 7 putfield #3 //值重新写回主内存 10 return
当我们进行i++的时候 大致分三部。
1.从主内存取值;
2.执行+1;
3.值重新写回主内存
如果仅仅用volatile关键字,在执行第二个指令的时候,其他线程执行了第一个指令,那么拿到的还是旧值。
有点数据库的事物那味道
2)阻止CPU指令重排序(JVM规范要求 对内存的时候加屏障)
指令重排序时不能把后面的指令重排到内存屏障之前的位置
https://www.cnblogs.com/ssskkk/p/12813115.html
volatile保证可见性的原理是添加内存屏障,而这些内存屏障在保障了线程之间的可见性的同时,也禁止了指令重排序。
CPU为什么会指令重排
假如有两条指令让CPU执行 并且两条指令直接没有前后依赖关系的时候,
在第一条指令的执行过程之中 如果需要从内存中读数据,可以先把第二条指令执行完,因为CPU的运算速度百倍于内存的读取速度
这么做可以增加计算器整个的运行效率
比如 我们在烧水的时候 可以洗碗一样, 虽然先烧水,但是洗碗的动作可能先执行完。
这个时候可能第二条指令会比第一条指令先执行完,原来的执行时1 2 背后CPU执行的顺序可能是 2 1(因为是两条指令 所以只有在并发的情况 才会出现这种可能)。
public static void main(String[] args) throws Exception{ int i=0; for (;;){ i++; x=0;y=0; a=0; b=0; Thread one = new Thread(() -> { a=1;x=b; }); Thread two = new Thread(() -> { b=1;y=a; }); one.start();two.start(); one.join(); two.join(); String result="第"+i+"次执行 ("+x+" "+y+")"; if (x==0&&y==0){ System.out.println(result); break; }else{ } } }
以上代码在运行了214609次之后,打印了出现了 x=0 y=0,证明了CPU存在指令重排序
对原子性的解释
java的原子性,不要和db原子性掺杂在一起去理解
java原子性 如果是保证任何时刻都只有一个线程执行