设计模式:单例模式的使用和实现(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,于是抛出异常
因为我没有过破坏单例模式的经历,所以也不知道为什么要搞这么复杂,只能说是很神奇。