设计模式:单例模式的使用和实现(JAVA)

单例模式的使用

jdk和Spring都有实现单例模式,这里举的例子是JDK中Runtime这个类

Runtime的使用

通过Runtime类可以获取JVM堆内存的信息,还可以调用它的方法进行GC。

public class Test {
    public static void main(String[] args) throws Exception {

        Runtime runtime = Runtime.getRuntime();
        runtime.gc();
        //jvm的堆内存总量
        System.out.println("堆内存总量" + runtime.totalMemory()/1024/1024 + "MB");
        //jvm视图使用的最大堆内存
        System.out.println("最大堆内存" + runtime.maxMemory()/1024/1024 + "MB");
        //jvm剩余可用的内存
        System.out.println("可用的内存" +runtime.freeMemory()/1024/1024 + "MB");

        Runtime runtime1 = Runtime.getRuntime();

        System.out.println(runtime == runtime1);
    }
}

这里创建了两个对象,通过等于号判断,两个引用来自同一个对象,确实是单例模式

 

 

Runtime的定义

这个类是介绍是:每一个Java应用有一个Runtime的实例,可以获取应用运行时的环境属性,当前的实例通过

getRuntime方法获取 。应用程序不能创建这个类的实例。

这差不多包含了单例类的定义,然后看一下这个类的内部实现

 

 很明显是一个标准的单例模式的(饿汉)实现,首先使用static修饰实例对象,所以类加载的时候就会创建实例,然后调用方法返回这个实例,使用private修饰构造函数。

 

反射破坏单例模式

Runtime类将构造函数私有化,就是不想让人创建它的实例,但是我们却可以使用反射来创建对象

public class Test {
    public static void main(String[] args) throws Exception {

            Class<?> clazz = Runtime.class;
        Constructor constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
        Object o1 = constructor.newInstance();
        Object o2 = Runtime.getRuntime();
            System.out.println(o1.getClass().getSimpleName());
            System.out.println(o2.getClass().getSimpleName());
            System.out.println(o1 == o2);

    }
}

通过运行结果可以看到,已经成功的创建了两个Runtime对象

 

至于破坏Runtime类的单例有什么坏处我也不知道,毕竟我是不会用反射去破坏它的,总之应该是有坏处的,下面看一下不能被反射破坏的单例模式实现

 

 

单例模式的实现

枚举类实现

使用枚举实现是因为JDK底层保护我们的枚举类不被反射,就解决了单例被反射破坏的问题

EnumSingleton.java

在枚举类中放了一个内部类(其实不放内部类也行)

public enum EnumSingleton {

    INSTANCE;
    class MyRuntime{

        public void hello(){
            System.out.println("hello");
        }
    }

    private MyRuntime myRuntime;

    EnumSingleton(){
        myRuntime  = new MyRuntime();
    }

    public MyRuntime getData(){
        return myRuntime;
    }

    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

下面测试一下这个单例 

public class Test {
    public static void main(String[] args) throws Exception {

        EnumSingleton.MyRuntime myRuntime = EnumSingleton.INSTANCE.getData();
        myRuntime.hello();
        EnumSingleton.MyRuntime myRuntime1 = EnumSingleton.getInstance().getData();
        System.out.println(myRuntime == myRuntime1);
    }
}

结果显而易见,单例模式已经成功实现

 

 

 至于使用反射测试枚举类,可以直接看一下JDK对枚举类的一个保护

使用反射创建对象,即调用Construct类的newInstance方法,这个方法里面已经定义了枚举对象不能被创建

 

 使用枚举实现单例的坏处有

  • 因为很少使用枚举类,所以用枚举创建单例感觉挺奇怪的。
  • 虽然它可以防止被反射破坏,但是它确实复杂。

 

像上面Runtime类那样的单例实现就差不多了,有一个缺点是,Runtime在类加载的时候就创建对象了

如果有很多类似的单例实现,在类加载时就创建了很多不需要的对象,会很占用资源

下面写一个懒汉式静态内部类单例实现(调用时才创建对象)

public class LazyInnerClassSingleton {

    static {
        System.out.println("加载静态代码块");
    }

    private LazyInnerClassSingleton(){

        System.out.println("创建对象成功");

    }

    public static void hello(){
        System.out.println("hello");
    }
/*
在调用getInstance方法时InnerLazy类被加载的才会初始化对象
 */
    public static LazyInnerClassSingleton getInstance(){
        return InnerLazy.LAZY;
    }

    private static class InnerLazy{

        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();

    }
}

这种实现的要点在与

  • 外部类构造方法私有化,无法创建外部类
  • 内部类的静态变量LAZY一直到调用外部类的getInstance方法时才会被加载,然后LAZY对象才会被创建,实现了懒加载
  • 注意内部类只是提供实例的一个工具,这里的单例对象是外部类

测试一下是不是真的

public class Test {

    public static void main(String[] args) throws Exception {

        LazyInnerClassSingleton.hello();
        System.out.println("开始创建对象实例");
        LazyInnerClassSingleton.getInstance();
    }
}

由运行结果看到,它只有在调用getInstance方法时才会创建对象,在加载外部类时是不会加载内部类的

 

 为了让它不被反射破坏,在构造方法上多加一个判断

 

无论是使用new关键字还是反射,都会调用类的构造方法,所以外部类使用这两种方式字创建实例,不然就会把异常抛出
因为if语句永远为true,虽然在执行if语句之前,InnerLazy.LAZY为null,但是只要使用了这个变量,就会去加载内部类
加载完内部类,InnerLazy.LAZY就不为null,于是抛出异常
posted @ 2021-08-19 13:41  划水的鱼dm  阅读(327)  评论(0编辑  收藏  举报