单例模式和volatile

Posted ssskkk

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式和volatile相关的知识,希望对你有一定的参考价值。

普通单例模式

饿汉式:利用static关键字,在类初始化的时候就会调用静态方法 

public class Singleton {
    private static  final Singleton singleton=new Singleton();
    private Singleton(){

    }
    public static Singleton getInstance(){
        return singleton;
    }
}

缺点:这个时候可能还没使用这个对象,浪费资源 (参考:类的初始化时机)

单例模式优化

懒汉式:声明为null,用到的时候 在初始化,但是需要加锁防止线程并发的时候,产生两个对象

public class Singleton {
    private static   Singleton singleton=null;
    private Singleton(){

    }
    public synchronized static Singleton getInstance(){
        if (singleton==null){
            System.out.println("我的其他业务");
            singleton=new Singleton();
        }
        return singleton;
    }
}

缺点:方法中有一些不需要上锁的业务代码也给锁上了,锁的粒度在粗了

深度优化(把锁细化)

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

public class Singleton {
    private static   Singleton singleton=null;
    private Singleton(){

    }
    public  static Singleton getInstance(){
        if (singleton==null){
            System.out.println("我的其他业务");
            synchronized (Singleton.class){//Double Check Lock
                if (singleton==null){
                    singleton=new Singleton();
                }
            }
        }
        return singleton;
    }
}

之所以DDL加两把锁: 防止多个线程同时执行到了业务逻辑

单例最终版本:CPU指令重排导致的安全性

因为CPU可能指令重排:声明变量的时候 需要加上volatile关键字

    private static  volatile Singleton singleton=null;

下面解释 为什么要加volatile关键字

在我们的idea中 idea-view-Show Bytecode可以看到我们方法的字节码 

比如

 Object o = new Object();

字节码

    NEW java/lang/Object
    DUP
    INVOKESPECIAL java/lang/Object.<init> ()V
    ASTORE 1

当我们new一个对象的时候,基本上分三步

1)在堆内存申请一个对象 分配一块内存,此时对象里面的值是一个默认值,

2)然后调用构造方法 初始化,

3)把堆内存的引用赋值给o  o来执行堆内存中的Object对象,建立关联

分配内存—初始化—建立关联

当一个线性1new的时候,走到第一步 分配内存时,发生了CPU的指令重排序,先建立关联(此时关联的对象 值都是空的 因为还没经过初始化),

这是线程2进来,发现对象不为空(因为第三步已经建立关联已经) 直接拿去用了 此时用的对象是一个半成品(因为线程1还没有进行初始化)

而volatile的作用有两点(这两点实现原理 下周会更新。。。)

1)多线程直接的可见性(缓存一致性协议 保持缓存行里的数据一致性)

2)阻止CPU指令重排序(JVM规范要求 对内存的时候加屏障)

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存在指令重排序

以上是关于单例模式和volatile的主要内容,如果未能解决你的问题,请参考以下文章

java的单例模式,为什么需要volatile

并发编程之单例模式,volatile和 synchronized

DCL 单例模式是否需要volatile?

优雅设计封装基于Okhttp3的网络框架:多线程单例模式优化 及 volatile构建者模式使用解析

关于单例模式中volatile的使用

面试突击51:为什么单例一定要加 volatile?