一、五种实现方式
-
饿汉式
public class HungrySingleton { private static HungrySingleton instance = new HungrySingleton(); private HungrySingleton(){ } public static HungrySingleton getInstance(){ return instance; } }
饿汉式单例模式:当加载这个类时就创建对象,以空间换时间,类的特性是在 JVM 中只加载一次,所以这个形式的单例模式是线程安全的。
-
懒汉式
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton(){ } public static LazySingleton getInstance(){ if (instance == null){ instance = new LazySingleton(); } return instance; } }
懒汉式单例模式:当需要创建对象时再创建,以时间换空间。这个形式的单例模式是线程不安全的,当有多个线程访问 getInstance 方法,会创建多个对象,是线程不安全的。所以引入了第三种单例模式:Double Check 懒汉式
-
Double Check 懒汉式
public class ConcurrentLazySingleton { private static ConcurrentLazySingleton instance = null; private ConcurrentLazySingleton(){ } public static ConcurrentLazySingleton getInstance(){ if (instance == null){ synchronized (ConcurrentLazySingleton.class){ if (instance == null){ instance = new ConcurrentLazySingleton(); // !!! } } } return instance; } }
Double Check 的方式保证了当多个线程访问 getInstance 方法时,同一时间只有一个线程可以访问该方法。第一个 if 判断避免了当对象已存在时,进行不必要的加锁。第二个 if 判断保证了单例对象只会存在一个。
但 DCL 方式的单例模式还是会有问题:以下这步创建对象的操作不是原子性的。
instance = new ConcurrentLazySingleton();
创建对象在 JVM 中的执行分为了四个步骤
- 在堆内存中开辟内存空间
- 在堆内存中实例化对象的各个参数(赋零值)
- 将必要的信息(类的元数据信息,对象的hash值,对象的gc分代年龄等)存储在对象的对象头中
- 执行 init<> 方法,生产对象
因为 JVM 是乱序执行指令的,当创建单例对象的时候,可能刚刚执行完第一步,就执行了第四步,那么这时候 instance == null 已经为 false 了,如果此时下一个进程进入方法,就会直接将内部信息为空的对象返回,造成异常。
所以 Java 引入了 volatile 关键字
volatile 修饰的变量,可以防止 JVM 指令重排序,保证了有序性。并且当这个变量被修改时,立刻更新到主内存,被所有线程共享,保证了可见性。
public class ConcurrentLazySingleton2 { private static volatile ConcurrentLazySingleton2 instance; private ConcurrentLazySingleton2(){ } public static ConcurrentLazySingleton2 getInstance(){ if (instance == null){ synchronized (ConcurrentLazySingleton2.class){ if (instance == null){ instance = new ConcurrentLazySingleton2(); } } } return instance; } }
-
静态内部类式
public class StaticInnerSingleton { private StaticInnerSingleton(){ } private static class Inner{ private static StaticInnerSingleton instance = new StaticInnerSingleton(); } public static StaticInnerSingleton getInstance(){ return Inner.instance; } }
静态内部类式单例模式:在类中封装了一个私有的静态内部类,当 StaticInnerSingleton 类被加载时,内部类并不会被加载。仅当内部类的静态成员(静态变量,静态方法,构造器)被调用时,类才会被加载,并且只加载一次。 与饿汉式单例模式不同,这样的方式延迟了对象的创建,仅当需要创建对象的时候才创建,节省了空间。与饿汉式单例模式相同,都保证了线程安全。
但这个形式也有缺陷,就是初始化时不能传参进去。
-
枚举
public enum EnumSingleton { instance("test"); private String name; EnumSingleton(String test) { } }
因为枚举中构造器默认为私有的,并且枚举实例创建时是线程安全的,所以保证了任一时刻只有一个单例对象。
二、反序列化破解单例模式
通过学习 Java 的序列化相关知识可以了解到,ObjectOutPutStream 可以将 A 对象保存为字节数组中,再由 ObjectInPutStream 将文件中读取出来还原为对象 B。A,B不相同。
那么单例对象提供序列化以及反序列化,会不会创建出第二个单例对象呢?
以下是测试代码:
public static void main(String[] args) {
SingletonDemo demo = SingletonDemo.getInstance();
demo.setTest("singletonTest");
System.out.println(demo);
ObjectOutputStream objectOutputStream = null;
objectOutputStream = new ObjectOutputStream(new FileOutputStream("singleton.txt"));
objectOutputStream.writeObject(demo);
ObjectInputStream objectInputStream = null;
File file = new File("singleton.txt");
objectInputStream = new ObjectInputStream(new FileInputStream(file));
SingletonDemo instance1 =(SingletonDemo) objectInputStream.readObject();
System.out.println(instance1);
...为省篇幅,将异常的捕捉以及流的关闭省略
}
输出为:
com.java_1217.singleton.SingletonDemo@6d6f6e28
com.java_1217.singleton.SingletonDemo@27d6c5e0
为什么可以破解?
因为实现了 Serializable 接口的单例类,通过反序列化创建对象时,调用的并不是单例类的构造方法,而是第一个非 Serializable 的无参构造方法。如果没有相应的父类,最终会找到 Object 类。
public class User extends Person implements Serializable {
private transient String name;
private Cat age;
public User() {
}
}
public class Person extends Human implements Serializable {
public Person() {
System.out.println("调用了Person父类的无参构造");
}
}
public class Human {
public Human() {
System.out.println("调用了Human父类的无参构造");
}
}
//通过反序列化创建 User 对象,控制台输出为 "调用了Human父类的无参构造"
如何避免?
Java 提供了名为 readResolve 的私有方法,当反序列化对象时,通过反射机制判断被反序列化的类是否含有 readResolve 方法,如果有的话,返回 readResolve 方法的返回值。
public class SingletonDemo implements Serializable {
private static SingletonDemo instance;
private SingletonDemo(){
System.out.println("创建了单例对象");
}
public static SingletonDemo getInstance(){
if (instance == null){
synchronized (SingletonDemo.class){
if (instance == null){
instance = new SingletonDemo();
}
}
}
return instance;
}
private Object readResolve() throws ObjectStreamException{
return instance;
}
}
在单例类中加入以上方法,再次测试,输出为:
com.java_1217.singleton.SingletonDemo@6d6f6e28
com.java_1217.singleton.SingletonDemo@6d6f6e28
三、暴力反射破解单例模式
通过学习反射相关知识,我们知道可以通过暴力反射的方式获取类中的私有化属性、方法、以及构造器。已知单例模式将构造器私有化,那么是否可以通过暴力反射的方式,获取被私有的构造器,并提供构造器来创建单例对象呢?
以下为测试代码:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException {
SingletonDemo instance = SingletonDemo.getInstance();
System.out.println(instance);
Class clazz = Class.forName("com.java_1217.singleton.SingletonDemo");
// Constructor constructor = clazz.getConstructor();
// 不能使用 getConstructor() 方法,因为 getConstructor() 方法只返回 public 类型的构造器
// 而 getDeclaredConstructor() 方法可以返回包括 public和 非public 类型的构造器
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
SingletonDemo newInstance = (SingletonDemo) constructor.newInstance(null);
System.out.println(newInstance);
}
//输出为:
创建了单例对象
com.java_1217.singleton.SingletonDemo@6d6f6e28
创建了单例对象
com.java_1217.singleton.SingletonDemo@135fbaa4
如何避免?
可以在私有的构造方法中,加入判断。
private SingletonDemo(){
if (instance != null){
throw new RuntimeException("不能访问私有构造方法!");
}
System.out.println("创建了单例对象");
}
再次测试结果为:
创建了单例对象
com.java_1217.singleton.SingletonDemo@6d6f6e28
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.java_1217.singleton.Test_reflect.main(Test_reflect.java:16)
Caused by: java.lang.RuntimeException: 不能访问私有构造方法!
at com.java_1217.singleton.SingletonDemo.<init>(SingletonDemo.java:13)
... 5 more
Process finished with exit code 1
参考资料:静态内部类单例原理
转载:https://blog.csdn.net/qq_44707077/article/details/115527620