单例模式和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)深度优化(把锁细化)

细化锁的时候需要加两次锁

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加两把锁: 防止多个线程同时执行到了加锁的代码

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

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{

            }
        }
    }
View Code

以上代码在运行了214609次之后,打印了出现了 x=0 y=0,证明了CPU存在指令重排序

原文地址:https://www.cnblogs.com/ssskkk/p/12708783.html