单例模式的使用
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,于是抛出异常。