0%

十、单例模式

单例模式的应用场景

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并 提供一个全局访问点。单例模式是创建型模式。在 J2EE 标准中,ServletContext、ServletContextConfig 等;在 Spring 框架应用中 ApplicationContext;数据库的连接池也都是单例形式。

饿汉式单例

饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线 程还没出现以前就是实例化了,不可能存在访问安全问题

优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
缺点:类加载的时候就初始化,不管用与不用都占着空间,可能会浪费内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

public class HungrySingleton {
/**
* 静态字段初始化实例
*/
private static final HungrySingleton INSTANCE = new HungrySingleton();
/**
* 私有化构造器
*/
private HungrySingleton(){}
/**
* 提供静态方法获取实例
* @return
*/
public static HungrySingleton getInstance(){
return INSTANCE;
}
}

懒汉式单例

懒汉式单例的特点是:被外部类调用的时候内部类才会加载。

double-check

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

public class LazySingleton {
/**
* 使用volatile防止因为指令重排序导致空指针异常。
*/
private static volatile LazySingleton instance;
private LazySingleton(){}
public static LazySingleton getInstance(){
// double-check加锁
if (instance == null) {
synchronized (LazySingleton.class){
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}

静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

public class InnerClassSingleton {
/**
* 私有化构造器
*/
private InnerClassSingleton(){}
public static InnerClassSingleton getInstance(){
// 在返回结果以前, 一定会先加载内部类
return InstanceHolder.INSTANCE;
}
/**
* 静态内部类在没有调用的时候,不会加载
*/
private static class InstanceHolder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
}

这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题。内部类一定是要在方 法调用之前初始化,巧妙地避免了线程安全问题。

反射破坏单例

上面介绍的单例模式的构造方法除了加上private以外,没有做任何处理。如果我们使用反射来调用其构造方法,然后,再调用getInstance()方法,应该就会两个不同的实例。

1
2
3
4
5
6
7
Class<InnerClassSingleton> clzz = InnerClassSingleton.class;
Constructor<InnerClassSingleton> constructor = clzz.getDeclaredConstructor(null);
constructor.setAccessible(true);
InnerClassSingleton o1 = constructor.newInstance();
InnerClassSingleton o2 = InnerClassSingleton.getInstance();
// 结果:false
System.out.println(o1 == o2);

解决方案:我们在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常。来看优化后的代码:

1
2
3
4
5
6

private InnerClassSingletonPreventReflect(){
if (InstanceHolder.INSTANCE != null) {
throw new RuntimeException("不允许构建多个实例!");
}
}

序列化破坏单例

当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例。

解决方案:只需要给单例对象增加 readResolve()方法即可。

1
2
3
private Object readResolve(){
return getInstance();
}

源码分析:
ObjectInputStream.readObject() -> readObject0(false)

54080286

重点就是readOrdinaryObject(unshared)方法
82299289

发现调用了ObjectStreamClassisInstantiable()方法,而isInstantiable()里面的代码如下:
82415056

代码非常简单,就是判断一下构造方法是否为空,构造方法不为空就返回 true。意味着,只要有无参构造方法就会实例化。
接着看readOrdinaryObject(unshared)方法
82844607

如果要反序列化的对象存在readResolve()方法,就调用该方法并将方法的返回结果作为readObject的返回结果。
因此,我们新增readResolve()方法,并返回当前单例实例即可防止反序列化破坏单例。
但是,我们通过分析源码以及调试,我们可以看到实际上实例化了两次,只不过新创建的对象没有被返回而已。那如果,创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大。

注册式单例

注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标 识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。

枚举式单例

1
2
3
4
5
6
7
8
9
10
11
12
public enum EnumSingleton {
INSTANCE;
/**
* 任意字段或者方法
*/
@Setter
@Getter
private Object field;
public void method() {
System.out.println("任意方法!");
}
}

使用jad反编译EnumSingleton.class文件,得到如下信息
53427927

  • 枚举EnumSingleton实际上被编译成了一个继承Enum的EnumSingleton类
  • 类的构造方法被私有了
  • 在静态代码块中,EnumSingleton就被实例化了。

综上,枚举式单例实际上也是一种饿汉式单例的实现。自然保证了线程安全相关问题。

枚举式单例避免反序列化破坏单例?

源码分析:

ObjectInputStream.readObject() -> readObject0(false)
54157927

我们看到在readObject0()中调用了readEnum()方法,来看readEnum()中代码实现:
54296048

我们发现枚举类型其实通过枚举名和 Class 对象类找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。

枚举式单例避免反射破坏单例?

Constructor.newInstance(..)源码:
54646102

在newInstance()方法中做了强制性的判断,如果修饰符是 Modifier.ENUM 枚举类型,直接抛出异常。
综上:枚举式单例从JDK层面保证了反序列化和反射时的安全性。枚举式单例也是《Effective Java》书中推荐的一种单例实现写法。

容器缓存单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ContainerSingleton {
private ContainerSingleton() {}
private static Map<String, Object> ioc = new ConcurrentHashMap<>();
public static <T> T getBean(Class<T> tClass) {
Object o = ioc.get(tClass.getName());
if (o != null) {
return (T) o;
}
synchronized (ioc) {
T t = null;
try {
t = tClass.newInstance();
ioc.put(tClass.getName(), t);
} catch (Exception e) {
e.printStackTrace();
}
return t;
}
}
}

容器式写法适用于创建多种类型单例的情况,便于统一管理,例如spring中的ioc容器。

源码:https://github.com/chentianming11/design-pattern
singleton包!