带你玩转单例模式(懒汉式,饿汉式,枚举)

欢迎访问我的个人博客网站(点击进入)

单例模式

1.单例模式简介

单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例。为了保证这个类只有一个实例,所以我们需要进行构造函数私有化,并通过其他的方法去获取实例

2.饿汉式

饿汉式,根据名字我们能想到,一个饥饿的人什么都想吃,所以饿汉式单例模式是通过静态变量提前声明的方式得到这个变量。
代码实现:

/**
 * @Author: LySong
 * @Date: 2020/3/30 21:31
 */
public class Hungry {

    //浪费空间
    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
    private byte[] data3 = new byte[1024*1024];
    private byte[] data4 = new byte[1024*1024];

    private Hungry(){

    }

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance(){
        return HUNGRY;
    }
}

弊端:

  • 对象的实例是静态的,就说明我们即使不适用这个对象,它也存在,那么如果这个变量占用内存很大,那么很显然会造成浪费

3.懒汉式

懒汉式单例模式,通过判断这个对象是否存在,如果存在就直接返回这个对象,将new对象延迟到了获得对象的方法中,实现了懒加载,解决了饿汉式可能造成空间浪费的弊端

private static LazyMan lazyMan
public static LazyMan getInstance(){      
        if(lazyMan == null){           
               lazyMan = new LazyMan();                     
        }
        return lazyMan;
    }

但是,这种写法在多线程环境下是不安全的,会产生线程安全问题,我们可以考虑给函数加锁,但是这种方式显然会使程序效率变低,所以我们引入了双重检查即DCL懒汉式

	private volatile static LazyMan lazyMan;

    public static LazyMan getInstance(){
        //双重检测
        if(lazyMan == null){
            synchronized (LazyMan.class){
                if(lazyMan == null){                  
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

代码说明:

  • 第一个检测对象是否为空,在里面套一个同步代码块,在同步代码块中再检测一次,这是为了防止出现多个线程几乎同时通过了第一层检测,那么他们一定会执行后面的new对象的操作,所以需要进行第二次检测
  • 在声明对象引用时,需要使用volatile关键字,这个关键字的作用是保证可见性,防止指令重排
  • 保证可见性:在多线程情况下,每个线程的工作内存中的数据可能会因为别的线程的修改而产生一些线程安全问题,所以需要保证对该引用的可见性才可以避免该情况的发生
    带你玩转单例模式(懒汉式,饿汉式,枚举)_第1张图片
  • 防止指令重排:在编译过程中,由于编译器会对源代码进行优化重排,执行顺序可能就不是我们期望的样子,因为new对象不是原子性操作,lazyMan = new LazyMan(),这句话其实需要进行三个步骤,分配内存空间,执行构造方法,初始化对象,把这个对象指向这个空间,由于指令重排的现象,很有可能一条线程new对象时,编译器会首先对这个引用开辟空间,那么后面来的线程会认为这个引用不为空,而直接返回,这时返回值会是空的。所以需要通过volatile关键字来防止指令重排

双重检测懒汉式的弊端:

  • 由于java中存在反射类,可以直接无视私有的构造方法去直接new对象,这样也会破坏单例模式
    测试实例:
public class LazyMan {

    private static boolean lysong = false;

    private LazyMan(){
        synchronized (LazyMan.class){
            if (lysong == false){
                lysong = true;
            }else{
                throw new RuntimeException("反射破坏单例异常");
            }
        }
        System.out.println(Thread.currentThread().getName() + "ok");
    }

    private volatile static LazyMan lazyMan;

    public static LazyMan getInstance(){
        //双重检测
        if(lazyMan == null){
            synchronized (LazyMan.class){
                if(lazyMan == null){
                    //不是一个原子性操作
                    /**
                     * 1.分配内存空间
                     * 2.执行构造方法,初始化对象
                     * 3.把这个对象指向这个空间
                     *  原本: 1 2 3
                     *  但是由于指令重排:1 3 2
                     * 此时 由于对象已经指向了内存空间,假如有线程B进入
                     * 就会认为此块内存空间已经有了,不为null
                     * 就会直接返回,这时返回值为null
                     */
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
    //多线程并发
    public static void main(String[] args) throws Exception {
//       LazyMan instance = LazyMan.getInstance();

        Field lysong = LazyMan.class.getDeclaredField("lysong");
        lysong.setAccessible(true);

        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazyMan instance1 = declaredConstructor.newInstance();
        lysong.set(instance1,false);
        LazyMan instance2 = declaredConstructor.newInstance();
        System.out.println(instance1 == instance2);
    }

}

所以引出了下面的方式完成单例模式,枚举

4.枚举

由于枚举类先天就是线程安全,且每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,正好可以用来完成单例模式,那么枚举是如何防止反射呢?我们可以打开反射创建的对象的源码(newInstance())看看
带你玩转单例模式(懒汉式,饿汉式,枚举)_第2张图片
所以我们通过枚举来实现单例模式:

/**
 * 枚举本身就是一个Class类
 * @Author: LySong
 * @Date: 2020/3/30 21:51
 */
public enum EnumSingle {

    INSTANCE;
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}
class Test{

    public static void main(String[] args) throws Exception {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();
        System.out.println(instance1 == instance2);

    }
}

通过测试也可以看出,枚举是安全的

5.总结

在饿汉式和懒汉式使用时需要注意构造器私有化,防止外部访问
懒汉式中需要注意的是,volatile关键字的使用
在单例模式中,由于反射机制和多线程的存在枚举是最安全的,但是懒汉式和饿汉式也不是不能用,需要根据实际的情况来灵活的选择

你可能感兴趣的:(设计模式,java,多线程,设计模式,jvm)