单例模式的探究---------懒汉式跟饿汉式

一、单例模式的探究

1.1 单例模式介绍

单例模式属于创建型模式的一种,应用于保证一个类仅有一个实例的场景下,并且提供了一个访问它的全局访问点,如spring中的全局访问点BeanFactory,spring下所有的bean都是单例。

单例模式的特点:从系统启动到终止,整个过程只会产生一个实例。

1.2 饿汉式

饿汉式:类加载的时候就实例化,并且创建单例对象。

类加载的方式是按需加载,且只加载一次。
因此,在单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用。也就是说,在线程访问单例对象之前就已经创建好了。由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例,也就是说,线程每次都只能也必定只可以拿到这个唯一的对象。因此就说,饿汉式单例天生就是线程安全的。

public class Hungry {

    //饿汉式,一上来就把对象加载了,有问题,比如耗费内存
    private String[] data = new String[2048];
    private String[] data2 = new String[2048];
    private String[] data3 = new String[2048];


    //构造器私有,一旦构造器私有了,别人就无法new这个对象
    //保证内存中只有一个对象
    private Hungry() {

    }
    //一开始就new一个对象,保证它是唯一的了
    private final static Hungry hungry = new Hungry();

    //静态保证可见性
    public static Hungry getInstance(){
        return hungry;
    }

}

 

1.3 懒汉式

1.3.1 在单线程模式下

懒汉式:默认不会实例化,什么时候用什么时候new。

public class LazyMan {

    //构造器私有,一旦构造器私有了,别人就无法new这个对象
    //保证内存中只有一个对象
    private LazyMan(){

    }

    //定义一个对象,并没有直接拿来使用
    private static LazyMan lazyMan;

    public static LazyMan getLazyMan(){
        //如果为空,则创建
        if(lazyMan == null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }

}

问:上面这段代码有什么问题呢?

答:单线程下是ok的,如果在并发多线程上,就有问题了。

1.3.2 在多线程模式下

public class LazyMan {

    //构造器私有,一旦构造器私有了,别人就无法new这个对象
    //保证内存中只有一个对象
    private LazyMan(){
        //测试线程创建情况
        System.out.println(Thread.currentThread().getName()+":启动成功");
    }

    //定义一个对象,并没有直接拿来使用
    private static LazyMan lazyMan;

    public static LazyMan getInstance(){
        //如果为空,则创建
        if(lazyMan == null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }

    //在多线程并发下,例如在8个线程下
    public static void main(String[] args){
        for (int i = 0; i < 8;i++){
            //启动线程
            new Thread(() -> {
                //启动之后,调用创建对象的方法
                LazyMan.getInstance();
            }).start();
        }
    }

}

测试结果:

单例模式的探究---------懒汉式跟饿汉式_第1张图片

从上面的结果来看的话,这个饿汉式单例模式,在多线程是有问题的,它不能保证只创建一次对象

 

1.3.3 双重检测锁模式

创建对象的时候,进行双重检测,并给对象加锁

public class LazyMan {

    //构造器私有,一旦构造器私有了,别人就无法new这个对象
    //保证内存中只有一个对象
    private LazyMan(){
        //测试线程创建情况
        System.out.println(Thread.currentThread().getName()+":启动成功");
    }

    //定义一个对象,并没有直接拿来使用
    private static LazyMan lazyMan;

    //懒汉式单例 DCL(双重检测锁模式)
    public static LazyMan getInstance(){
        //如果为空,则给对象加锁
        if(lazyMan == null) {
            //锁住当前对象
            synchronized (LazyMan.class) {
                //加了锁之后,再次判断,如果为空,则创建对象
                if(lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    //在多线程并发下,例如在8个线程下
    public static void main(String[] args){
        for (int i = 0; i < 8;i++){
            //启动线程
            new Thread(() -> {
                //启动之后,调用创建对象的方法
                LazyMan.getInstance();
            }).start();
        }
    }

}

测试结果:

单例模式的探究---------懒汉式跟饿汉式_第2张图片

 

问:上面这个双重检测操作有什么问题?

答:双重检测锁是原子性操作,每次创建对象都会经历分配内存、执行构造方法(也就是初始化对象)、把对象指向内存空间

问:应该怎么解决指令重排?

1.3.4 双重检测锁模式+volatile

public class LazyMan {

    //构造器私有,一旦构造器私有了,别人就无法new这个对象
    //保证内存中只有一个对象
    private LazyMan(){
        //测试线程创建情况
        System.out.println(Thread.currentThread().getName()+":启动成功");
    }

    //定义一个对象,并没有直接拿来使用
    //volatile,多线程并发工作区变量的可见性,避免指令重排
    private volatile static LazyMan lazyMan;

    //懒汉式单例 DCL(双重检测锁模式)
    public static LazyMan getInstance(){
        //如果为空,则给对象加锁
        if(lazyMan == null) {
            //锁住当前对象
            synchronized (LazyMan.class) {
                //加了锁之后,再次判断,如果为空,则创建对象
                if(lazyMan == null){
                    lazyMan = new LazyMan();//不是一个原子性操作
                    /**
                     * 不是原子性操作,每次创建对象,就会有下面三个步骤
                     * 这三个步骤,与可能会发生指令重排过程,比如希望执行123,但是可能会出现132
                     * 1.分配内存空间
                     * 2.执行构造方法,也就是初始化对象
                     * 3.把这个对象指向这个内存空间
                     * 避免指令重排,在定义对象的时候加上volatile
                     */

                }
            }
        }
        return lazyMan;
    }

    //在多线程并发下,例如在8个线程下
    public static void main(String[] args){
        for (int i = 0; i < 8;i++){
            //启动线程
            new Thread(() -> {
                //启动之后,调用创建对象的方法
                LazyMan.getInstance();
            }).start();
        }
    }

}

问:这种模式可以破解吗?

答:可以,利用反射模式,让构造器私有失效

根据单例模式源码分析,利用enum枚举可以阻止单例模式被破坏

单例模式的探究---------懒汉式跟饿汉式_第3张图片

 

1.3.5 最终在枚举模式下

//enum其实本身也是一个class类
public enum EnumSingle {

    //定义一个枚举变量,instance
    instance;

    //返回定义的枚举变量
    public EnumSingle getInstance(){
        return instance;
    }

}

class TestEnum{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //获取自定义的枚举变量
        EnumSingle instance1 = EnumSingle.instance;

        //用反射动态获取构造方法,注意,这里的枚举构造方法是有参构造方法,如果是无参的话,会出现下面这种报错
        //java.lang.NoSuchMethodException: de.wen.dewen.single.EnumSingle.()
        Constructor declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);

        //让构造方法私有给失效
        declaredConstructor.setAccessible(true);

        //试图利用反射破坏单例模式
        EnumSingle instance2 = declaredConstructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);

    }
}

运行结果:

你可能感兴趣的:(单例模式的探究---------懒汉式跟饿汉式)