创建型模式之单例模式

创建型模式(Creational Pattern)是对类的实例化过程的抽象化,能够提供对象的创建和管理职责。创建型模式共有5种:
■ 单例模式;
■ 工厂方法模式;
■ 抽象工厂模式;
■ 建造者模式;
■ 原型模式。

含义

单例模式的英文原话是:
Ensure a class has only one instance,and provide a global point of access to it.

单例模式的主要作用是确保一个类只有一个实例存在。单例模式可以用在建立目录、数据库连接、线程池等需要单线程操作的场合,用于实现对系统资源的控制。

单例类中一个最重要的特点是类的构造函数是私有的,从而避免外界利用构造函数直接创建出任意多的实例。另外需要注意的是,由于构造函数是私有的,因此该类不能被继承。

实现方法

懒汉式单例类

第一次引用类时,才进行对象实例化。

延迟加载,只有在真正使用的时候,才开始实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class LazySingleton {
private volatile static LazySingleton instance;

private LazySingleton(){}

public static LazySingleton getInstance(){
if (instance == null){
synchronized (LazySingleton.class){
if(instance == null){
instance = new LazySingleton();
}
}
}
return instance;
}
}

//测试类
public class Test {
public static void main(String[] args) {
new Thread(()->{
LazySingleton instance = LazySingleton.getInstance();
System.out.println(instance);
}).start();

new Thread(()->{
LazySingleton instance = LazySingleton.getInstance();
System.out.println(instance);
}).start();
}
}

synchronized 保证多线程安全,如果直接加在方法上,不管有没有实例化都会加一次锁,会有性能的损耗,所以放在方法内部,用double check加锁优化,有点类似Peterson算法。

同时还要用volatile避免空指针问题

为什么可能会发生空指针问题呢?先来看一下编译的原理,对于语句Demo demo = new Demo(); 对编译后的.class文件用命令javap -v Demo.class 进行反汇编

可以看到标准的过程是:分配空间 -> 初始化 -> 引用赋值。

然而,由于JVM有即时编译功能,CPU也有流水线并行,可能第一个线程在未初始化之前先进行赋值,导致第二线程返回空指针, 所以用volatile 禁用字节码重排序。

饿汉式单例类

类加载时,就进行对象实例化

基于JVM类加载机制保证线程安全。 类加载的初始化阶段就完成了实例的初始化。

类加载的时机:当前类是启动类,直接进行new操作,访问静态属性/方法,用反射访问类,初始化一个类的子类等

类加载的过程:

  1. 加载二进制数据到内存中,生成对应的Class
  2. 连接:a.验证 b.准备(给类的静态成员变量赋默认值) c.解析
  3. 初始化:给类的静态变量赋初值
1
2
3
4
5
6
7
8
9
public class HungrySingleton {
private static HungrySingleton instance = new HungrySingleton();

private HungrySingleton(){}

public static HungrySingleton getInstance(){
return instance;
}
}

静态内部类

基于JVM的懒加载机制,静态内部类被调用的时候才会被加载。如果是非静态内部类,在创建内部类实例之前,必须首先创建外围类的实例,所以用静态类比较合适

1
2
3
4
5
6
7
8
9
10
11
public class InnerSingleton {
private static class InnerClassHolder{
private static InnerSingleton instance = new InnerSingleton();
}

private InnerSingleton(){}

public static InnerSingleton getInstance(){
return InnerClassHolder.instance;
}
}

反射攻击问题

以上三种方法存在的问题是,如果拿到了构造函数显示创建,仍然可创建出多个实例来,即反射攻击

以静态内部类为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

Constructor<InnerSingleton> declaredConstructor = InnerSingleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
InnerSingleton innerSingleton = declaredConstructor.newInstance();
System.out.println(innerSingleton);

new Thread(()->{
InnerSingleton instance = InnerSingleton.getInstance();
System.out.println(instance);
}).start();
}
}
//测试结果
InnerSingleton@42a57993
InnerSingleton@b79138

对于饿汉模式和静态内部类,一种解决办法是在构造函数中添加判断条件。但是懒汉模式就不行了

1
2
3
private InnerSingleton(){
if(InnerClassHolder.instance != null) throw new RuntimeException(" 此为单例,不允许重复创建 ");
}

通过查看newInstance() 源码,发现了一个判断条件,不能为枚举类型 , 说明反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。

枚举类型

“单元素的枚举类型已经成为实现Singleton的最佳方法”

1
2
3
4
5
6
7
public enum EnumSingleton {
INSTANCE;

public void print(){
System.out.println(this.hashCode());
}
}

序列化问题

如果一个单例的类实现了Serializable或Cloneable接口,当反序列化或克隆时可能会创建出一个新的实例来,因为还原的时候并不会调用类的构造函数。

解决方法一是在序列化时加上一个版本号,同时实现readResolve()方法,该方法返回单例对象

另一种解决方法仍然是使用枚举类型,天然支持反序列。

用枚举类型解决反射攻击和序列化问题

应用场景

■ 要求生成唯一序列号的环境。
■ 在整个项目中需要一个共享访问点或共享数据,例如,一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的。
■ 创建一个对象需要消耗的资源过多,如访问IO和数据库等资源。
■ 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(也可以采用直接声明为static的方式)。

在Spring框架中,每个Bean 默认就是单例的;Java 基础类库中的 java.lang.Runtime 类也采用了单例模式,其getRuntime()方法返回了唯一的实例。

注意事项

■ 单例模式与单一职责原则有冲突。
■ 单例类仅局限于一个JVM,因此当多个JVM的分布式系统时,这个单例类就会在多个JVM中被实例化,造成多个单例对象的出现。如果是无状态的单例类,则没有问题,因为这些单例对象是没有区别的。如果是有状态的单例类,则会出现问题,例如,给系统提供一个唯一的序列号,此时序列号不唯一,可能出现多次。因此,在任何使用EJB、RMI和JINI技术的分布式系统中,应当避免使用有状态的单例类。
■ 同一个JVM中会有多个类加载器,当两个类加载器同时加载同一个类时,会出现两个实例,此时也应尽量避免使用有状态的单例类。
■ 使用单例模式时,需要注意序列化和克隆对实例唯一性的影响。如果一个单例的类实现了Serializable或Cloneable接口,则有可能被反序列化或克隆出一个新的实例来,从而破坏了“唯一实例”的要求,因此,通常单例类不需要实现Serializable和Cloneable接口