单例模式

单例设计模式(Singleton Design Pattern)就是一个类只允许创建一个对象。

为什么要使用单例?

  1. 单例可以表示全局唯一,一些数据在系统中应该且只能保存一份,那就应该设计为单例类,例:配置类,全局计数器
  2. 处理资源访问冲突(如日志写文件如果多线程同时写就会导致覆盖问题)

如何获取一个单例

常见的单例获取方式大致可以分为懒汉式和饿汉式,除此外还有其他获取的方式,但是无论是哪种方式都要注意以下几点

  1. 构造器需要私有化
  2. 暴露公共接口获取单例对象
  3. 是否支持懒加载
  4. 是否线程安全

下面讲讲单例模式的几种实现方式

饿汉式

饿汉式的实现方法比较简单,在类加载的时候已经创建完毕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class EagerSingleton implements Serializable {
private static EagerSingleton instance = new EagerSingleton();

/**
* 私有构造函数
*/
private EagerSingleton() {
synchronized (EagerSingleton.class){
if (instance != null){
throw new RuntimeException("单例构造器禁止反射调用!!!");
}
}
}

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

饿汉式对象在类加载的时候被创建,所以没有线程不安全的问题,但是如果对象过大而且没有被使用,会占用较多内存资源,也会增加程序初始化开销(但是如果对象过大等到使用的时候再创建就会等待时间过长问题,还是要根据具体业务决定)

懒汉式-线程不安全

懒汉式就是等到真正需要对象的时候才创建,而不是一开始就创建好,实现代码如下

1
2
3
4
5
6
7
8
9
10
public class LazySingleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance null) {
instance = new Singleton();
}
return instance;
}
}

但是上面的代码有一个很大的问题,就是在并发量大的情况下,可能回同时创建多个单例对象,无法满足单例特点

懒汉式-线程安全

要解决懒汉式线程不安全问题也很简单,加锁就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LazySingleton {
private static LazySingleton instance = null;

private LazySingleton() {
}

public synchronized static LazySingleton getInstance() {
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 DclSingleton {
private static volatile DclSingleton instance;

private DclSingleton() {
}

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

单例对象加入volatile是为了防止重排序导致对象半初始化情况(在高JDK已解决这个问题)

静态内部类

静态内部类实现单例既能够满足延迟加载 ,内部对象在类加载的时候不会被创建,只有在调用获取单例对象方法的时候才会加载内部类,又能保证线程安全,主要是由JVM内部保证单例

1
2
3
4
5
6
7
8
9
10
11
12
13
public class InnerSingleton {
private InnerSingleton(){
}

private static class SingletonHolder{
private static final InnerSingleton INSTANCE = new InnerSingleton();
}

public static InnerSingleton getInstance(){
return SingletonHolder.INSTANCE;
}
}

枚举单例

1
2
3
public enum EnumSingleton {
INSTANCE;
}

这种实现方式是通过Java枚举的特性来保证单例的

以上单例模式方法真的能够保证单例吗

答案显然是否定的,我们可以通过反射和序列化继续创建对象

反射

除了枚举外,其他实现方式均能通过反射获取构造器并创建新的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// 获取类对象
Class eagerSingletonClass = EagerSingleton.class;
//获取构造器
Constructor declaredConstructor = eagerSingletonClass.getDeclaredConstructor();
//取消访问权限限制
declaredConstructor.setAccessible(true);
//创建对象
EagerSingleton eagerSingleton = declaredConstructor.newInstance();
EagerSingleton eagerSingleton2 = declaredConstructor.newInstance();
//判断是否是同一个对象
System.out.println(eagerSingleton eagerSingleton2);
}
}

img

怎么解决这个问题呢,既然反射获取构造器不能阻止,那就只能在构造器做一些处理了,如果对象已经创建还调用构造方法,那就直接抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class EagerSingleton implements Serializable {
private static EagerSingleton instance = new EagerSingleton();

/**
* 私有构造函数
*/
private EagerSingleton() {
synchronized (EagerSingleton.class){
if (instance != null){
throw new RuntimeException("单例构造器禁止反射调用!!!");
}
}
}

public static EagerSingleton getInstance() {
return instance;
}
private Object readResolve() {
return instance;
}
}

序列化和反序列化

解决了反射构造对象难道就能保证单例了吗,其实我们通过对象的序列化和反序列化还是可以创建新的对象

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) throws IOException, ClassNotFoundException {
EagerSingleton singleton = EagerSingleton.getInstance();
FileOutputStream fout = new FileOutputStream("D://singleton.txt");
ObjectOutputStream out = new ObjectOutputStream(fout);
out.writeObject(singleton);
// 将实例反序列化出来
FileInputStream fin = new FileInputStream("D://singleton.txt");
ObjectInputStream in = new ObjectInputStream(fin);
Object o = in.readObject();
System.out.println(osingleton);
}

img

要解决这个问题,就只能在反序列化的时候做一些改动,在反序列化的过程中,会执行readResolve方法,并将返回值作为反序列化的结果,所以我们可以重写这个方法,返回单例对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class EagerSingleton implements Serializable {
private static EagerSingleton instance = new EagerSingleton();

/**
* 私有构造函数
*/
private EagerSingleton() {
synchronized (EagerSingleton.class){
if (instance != null){
throw new RuntimeException("单例构造器禁止反射调用!!!");
}
}
}

public static EagerSingleton getInstance() {
return instance;
}
// 重写方法
private Object readResolve() {
return instance;
}
}

img

单例模式的应用

在Java中严格的单例模式其实很少,jdk中的Runtime类和mybatis的vfs(查找指定路径的资源)就是单例

实际的开发中我们很少严格的用到单例模式 因为他有一些弊端

  1. 无法面向对象编程:单例导致构造器私有化让类无法成为其他类的父类,等于放弃了继承和多态性,难以扩展需求
  2. 难以横向扩展:当单例对象无法满足需求的时候没办法扩展

但是我们可以借鉴这种思想,例如线程级别的单例(LocalThread)和容器级别的单例(IOC)