如何手写单例
# 普通的单例
单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象
class Single {
//1,构造一个该类的实例对象
private static Single sin = new Single();
//2,私有化构造方法
private Single(){}
//3,定义一个返回函数
public static Single getInstance(){
return sin;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
按照以上三步,就可以写出一个最普通的单例,以下所有的单例,都是这个的变种
这个普通的单例叫饿汉式单例,它的特点是在类加载时就初始化
# 懒汉式单例
懒汉式只有催促它它才会干活,第一次调用它才会创建实例对象,代码如下:
class Single {
private static Single sin;
private Single(){}
public static Single getInstance(){
if(sin == null){
sin = new Single();
}
return sin;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
但这么做一定是线程不安全的,如果想要保证线程安全,最简单的方法是加上synchronized
class Single {
private static Single sin;
private Single(){}
public static synchronized Single getInstance(){
if(sin == null){
sin = new Single();
}
return sin;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
以上代码保证了线程安全,但是效率不敢恭维,原因是每一次调用这个方法都需要对这个类的class对象上锁
# 双重校验锁 DLC
这种方式采用双锁机制,安全且在多线程情况下能保持高性能
class Single {
private volatile static Single sin;
private Single(){}
public static Single getInstance(){
if(sin == null){
synchronized (Single.class){
if (sin == null){
sin = new Single();
}
}
}
return sin;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这段代码理解起来有些复杂,有两个问题需要解决
# 为什么 getInstance 中有两个一样的判断
因为只加一个if无法解决线程安全问题
可能有多个线程同时满足外部的if判断sin == null,进入的生成对象的代码块,如果没有内部的if,那么它们还是会先后生成多个对象
# 为什么要加volatile,去掉行不行
不行,必须加
因为字节码在编译为机器语言时会发生指令重排序
在机器语言生成一个对象会进行如下操作: 1,初始化这个对象 2,调用这个对象的构造方法 3,将引用指向这个对象
发生指令重排序的时候,3与2可能会进行重排,那么这个对象的生成顺序变成了1-3-2
此时,如果执行完3没有执行2时,另外一个线程调用了getInstance,是可以得到一个没有被赋值的半成品的,使用这个半成品必然会出现问题
# 如何破坏单例
# 使用反射破坏
通过反射获得单例类的构造函数,由于该构造函数是private的,通过setAccessible(true)指示反射的对象在使用时应该取消 Java 语言访问检查,使得私有的构造函数能够被访问,这样使得单例模式失效
如何解决问题:防止构造函数被成功调用两次。在构造函数中对实例化次数进行统计,大于一次就抛出异常
# 使用序列化破坏
在单例实现序列化接口之后,序列化和反序列化后得到的对象和单例中的对象不是同一个,原因是因为序列化会通过反射调用无参数的构造方法创建一个新的对象
public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton singleton1 = Singleton.getInstance();
// 将 singleton1 对象序列化到文件
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
out.writeObject(singleton1);
out.close();
// 从文件中反序列化对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.ser"));
Singleton singleton2 = (Singleton) in.readObject();
in.close();
// 比较两个对象是否相等
System.out.println("singleton1 hashCode: " + singleton1.hashCode());
System.out.println("singleton2 hashCode: " + singleton2.hashCode());
System.out.println("Are they the same instance? " + (singleton1 == singleton2));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
上述代码返回为 false
如何解决问题:序列化接口源码文档中标注了一个返回序列化对象的方法叫 readResolve, 为 SingletonSerializable 重写一个 readResolve 方法,直接返回单例对象即可
private Object readResolve() throws ObjectStreamException {
return instance;
}
2
3
# 枚举单例模式如何防止反射攻击
枚举也是 java 类,对枚举类的 class 进行反编译,可以得到一个新的抽象类,这个抽象类事实上就是实现枚举的
由于抽象类不能被实例化,自然防止了反射破坏单例条件