[设计模式]单例模式

Posted baihan

tags:

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

[设计模式]单例模式

一、饿汉式

public class Hungry {

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

    private Hungry(){

    }

    private static Hungry hungry = new Hungry();

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

饿汉式在类被初始化时就已经在内存中创建了对象,以空间换时间,故不存在线程安全问题

二、懒汉式

public class Lazy {

	private static Lazy lazy;//默认为null
	
    private Lazy() {
      
    }

    public static Lazy getInstance() {
    	if (lazy == null) {
        	lazy = new Lazy();
    	}
        return lazy;
	}

懒汉式在方法被调用后才创建对象,以时间换空间,在多线程环境下存在风险

三、DCL懒汉式(双重检测加锁)

public class Lazy {

    private Lazy() {
    
    }

    private static Lazy lazy;

    //DCL懒汉式(双重检测锁)
    public static Lazy getInstance() {
        if (lazy == null) {
            synchronized (Lazy.class) {
                if (lazy == null) {
                    lazy = new Lazy();
                }
            }
        }
        return lazy;
    }
  • DCL模式的优点就是,只有在对象需要被使用时才创建。

  • 第一次判断 INSTANCE == null避免非必要加锁的情况,这样可以提高执行效率

  • 使用synchronized锁住实例对象是保证程序在多线程环境下的安全性

  • 由于JVM存在乱序执行功能(指令重排),DCL也会出现在多线程场景下不安全的情况。例如:Lazy lazy = new Lazy();这一段代码不是原子性操作,在JVM里分为三步:

    1. 在堆内存中开辟存储空间。
    2. 在堆内存中实例化Lazy里面的各个参数。
    3. 把对象指向堆内存空间。

    解决办法: private volatile static Lazy lazy;加一个volatile关键字可以避免指令重排

四、静态内部类模式

public class SingleTon{
    private SingleTon(){}

    private static class SingleTonHolder {
        private final static SingleTon INSTANCE = new SingleTon();
    }

    public static SingleTon getInstance(){
        return SingleTonHolder.INSTANCE;
    }
}
  • 外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不浪费内存。

  • 即当SingleTon第一次被加载时,并不需要去加载SingleTonHolder,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE.

  • 第一次调用getInstance()方法会使虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化

  • 静态内部类单例模式的核心原理为对于一个类,JVM在仅用一个类加载器加载它时,静态变量的赋值在全局只会执行一次

  • 静态内部类也有缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部不方便传递参数进去。

五、利用反射破坏单例模式

这里会用代码来展示反射是如何破坏DCL饿汉模式的。

  • 第一场对决

    DCL方:

    public class Lazy {
    
        private Lazy() {
        }
    
        private volatile static Lazy lazy;//volatile避免指令重排
    
        //DCL懒汉式(双重检测锁)
        public static Lazy getInstance() {
            if (lazy == null) {
                synchronized (Lazy.class) {
                    if (lazy == null) {
                        lazy = new Lazy();//不是原子性操作
                    }
                }
            }
            return lazy;
        }
    }
    

    反射方:

     public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
            
            Lazy lazy1 = Lazy.getInstance();
            
         	//反射开始操作
            Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);//获取构造器
            declaredConstructor.setAccessible(true);//无视私有构造器
            Lazy lazy2 = declaredConstructor.newInstance();//通过构造器创建另一个实例
            System.out.println(lazy1.hashCode());//输出实例对象的哈希值
            System.out.println(lazy2.hashCode());
        }
    

    结果:出现了两个不同的实例对象,反射赢。

    460141958
    1163157884
    
  • 第二场对决

    DCL方:

    public class Lazy {
    
        //在构造器加了锁和条件判断
        private Lazy() {
            synchronized (Lazy.class){
                if(lazy != null){
                    throw new RuntimeException("不要试图使用反射破坏单例模式!");
                }
            }
        }
    
        private volatile static Lazy lazy;//volatile确保原子性,避免指令重排
    
        //DCL懒汉式(双重检测锁)
        public static Lazy getInstance() {
            if (lazy == null) {
                synchronized (Lazy.class) {
                    if (lazy == null) {
                        lazy = new Lazy();//不是原子性操作
                    }
                }
            }
            return lazy;
        }
    

    反射方:

      public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            
            Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
            declaredConstructor.setAccessible(true);//无视私有构造器
          
          	//两次都使用反射创建对象
            Lazy lazy1 = declaredConstructor.newInstance();
         	Lazy lazy2 = declaredConstructor.newInstance();
    
            System.out.println(lazy1.hashCode());
            System.out.println(lazy2.hashCode());
    
        }
    }
    

    结果:出现了两个不同的实例对象,反射赢。

    460141958
    1163157884
    
  • 第三场对决

    DCL方:

    public class Lazy {
    
        //加入标志位
        private static boolean flag = false;
    
        private Lazy() {
            //进行标志位判断
            if(flag == false){
                flag = true;
            }
            else {
                throw new RuntimeException("不要试图使用反射破坏单例模式!");
            }
        }
    
        private volatile static Lazy lazy;//volatile确保原子性,避免指令重排
    
        //DCL懒汉式(双重检测锁)
        public static Lazy getInstance() {
            if (lazy == null) {
                synchronized (Lazy.class) {
                    if (lazy == null) {
                        lazy = new Lazy();//不是原子性操作
                    }
                }
            }
            return lazy;
        }
    

    反射方:

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
            Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
            declaredConstructor.setAccessible(true);//无视私有构造器
            Lazy lazy1 = declaredConstructor.newInstance();
    
            Field flag = Lazy.class.getDeclaredField("flag");//通过反射获取标志位字段
            flag.setAccessible(true);//破坏字段私有性
    
            flag.set(lazy1,false);//再将标志位改回false
    
            Lazy lazy2 = declaredConstructor.newInstance();
    
            System.out.println(lazy1.hashCode());
            System.out.println(lazy2.hashCode());
        }
    

    结果:出现了两个不同的实例对象,反射赢。

    1163157884
    1956725890
    

    综上,反射完胜!

六、枚举实现单例模式

枚举方:

//枚举本身也是一个类
public enum Enum {

    INSTANCE;
    public Enum getInstance(){
        return INSTANCE;
    }

}

class Test{
    public static void main(String[] args) {
        Enum instance1 = Enum.INSTANCE;
        Enum instance2 = Enum.INSTANCE;

        //经检测,instance1和instance2是同一个对象。
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

以上就是使用枚举的方式实现单例模式,它的确可以保证一个类只有一个实例对象。我们现在唯一担心的就是反射。反射那么厉害,它可以破坏这种单例模式吗?让我们来试一下。

  • 首选,我们先找到这个文件编译过后的字节码文件Enum.class

  • 然后利用反编译工具将此字节码文件反编译成java文件。

    • 使用Idea自带的反编译工具。

      技术图片

    • 使用命令行javap -p Enum.class

      技术图片

    • 使用第三方反编译工具jad的命令jad -sjava Enum.class

      技术图片

  • jad反编译后的文件最准确,所以Enum类的构造器是private Enum(String s, int i){super(s,i);}

反射方:

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<Enum> declaredConstructor = Enum.class.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);

        Enum enum1 = declaredConstructor.newInstance();
        System.out.println(enum1.hashCode());

        Enum enum2 = declaredConstructor.newInstance();
        System.out.println(enum2.hashCode());
    }

结果:报错了,无法通过反射的方法破坏单例模式,枚举胜!

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects

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

常用代码片段

性能比较好的单例写法

你熟悉的设计模式都有哪些?写出单例模式的实现代码

单例模式以及静态代码块

设计模式之单例模式

设计模式之单例模式