设计模式——单例模式

单例模式是一种比较常见的设计模式,也是最简单的设计模式之一,属于创建性模式。

单例模式中,简而言之就是单例对象的类只允许有一个实例对象存在。其概念及适用场景很容易理解,举个栗子,在公司中只有一台饮水机,每个员工需要喝水时都需要先获取饮水机的使用权然后才能接水。在上述例子中,饮水机就是单例类饮水机的实例对象。

本文着重阐述单例模式的几种实现方式

单例模式有3个特点

  1. 构造方法私有化
  2. 实例化的变量引用私有化
  3. 获取实例的方法共有

依据上述三个特点,就产生了如下代码

/**
 * 饿汉式
 * 单例的初始化交给类初始化时去完成
 */
public class StaticVarSingleton {
    private final static StaticVarSingleton INSTANCE = new StaticVarSingleton();

    private StaticVarSingleton() {}

    public static StaticVarSingleton getInstance() {
        return INSTANCE;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

上述代码实现了单例模式的3个特点,但是如果这个类一直都没有被使用到那岂不是白白的浪费了这些空间,于是我们对其做一些修改,完成代码如下

/**
 * 懒汉式
 * 第一次使用时加载
 */
public class SimpleSingleton {
    //-----修改点1-----
    private static SimpleSingleton INSTANCE;

    private SimpleSingleton() {}

    public static SimpleSingleton getInstance() {
        //-----修改点2-----
        if(INSTANCE == null){
            INSTANCE = new SimpleSingleton();
        }
        return INSTANCE;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

ok,这样我们就完成了在其被使用之前不会浪费内存的操作

其实在这里可以引入两个概念:懒汉式和饿汉式,这是单例模式的两个分类。

懒汉式:jvm启动时不实例对象,当第一次使用到时进行实例化

饿汉式:jvm启动就实例化对象

我们回过头来再看一看刚刚的SimpleSingleton代码,从并发的角度来看,它是线程不安全的,当Thread1在执行INSTANCE = new SimpleSingleton();时,Thread2进入判断INSTANCE依然为null,这时Thread2也会new一个SimpleSingleton,此时就会存在线程安全问题。我们改进一下

/**
 * 懒汉式
 * 第一次使用时加载
 */
public class SimpleSingleton {
    private static SimpleSingleton INSTANCE;

    private SimpleSingleton() {}
	//-----改进点-----
    public synchronized static SimpleSingleton getInstance() {
        if(INSTANCE == null){
            INSTANCE = new SimpleSingleton();
        }
        return INSTANCE;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

我们在方法上添加了synchronized关键字后确实解决了线程安全的问题,但同样会造成另一个问题——性能下降。那么再换一种做法

/**
 * 懒汉式
 * 第一次使用时加载
 * 双重检查保证线程安全
 */
public class DoubleCheckSingleton {
 	//-----改进点1-----
    private volatile static DoubleCheckSingleton INSTANCE;

    private DoubleCheckSingleton() {}

    public static DoubleCheckSingleton getInstance() {
        if(INSTANCE == null){
            //-----改进点2-----
            synchronized(DoubleCheckSingleton.class){
                if(INSTANCE == null){
                    INSTANCE = new DoubleCheckSingleton();
                }
            }
        }
        return INSTANCE;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这一次我们使用的双重检查的方式,成功的实现了一个线程安全的懒汉式单例,那么除了这种方法之外,还有什么别的方式来保证其线程安全吗?

/**
 * 懒汉式
 * 第一次使用时加载
 * 利用JVM保证线程安全
 */
public class InnerClassSingleton {

    private static InnerClassSingleton INSTANCE;

    private InnerClassSingleton() {}

    public static InnerClassSingleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
    /**
     * 内部类中保持实例
     */
    private static class SingletonInstance {
        private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

JVM可以保证类加载时是线程安全的,所有我们定义一个内部类,在用到时JVM会对内部类进行加载,并且保证其线程安全。

以上就是我们比较经常用的几种实现单例模式的方式,but!还是会存在两个问题

  1. 如果直接使用反射,是不是就可以获得另一个INSTANCE
  2. 这些单例实例都是不支持序列化的

当然我们手撕代码也是可以解决上述两个问题,但是自jdk1.5之后,Java为我们提供了一个更好的解决思路——enum

引用《Effective Java》一书中的话——单元素的枚举类型已经成为实现Singleton的最佳方法。那么接下来就来介绍一下使用枚举类实现单例

public enum Singleton {
    INSTATNCE;
        
    public Singleton getInstance(){
        return INSTATNCE;
    }
}
1
2
3
4
5
6
7

ok,代码简介明了,并没有想象中的一大堆代码

总结

实现分类
  • 懒汉式
    • 获取实例方法前加synchronized
    • 双重检查
    • 内部类
  • 饿汉式
    • 静态变量赋值
    • 静态代码块为静态变量赋值
  • 最优单例实现:enum
单例模式的实现中考虑的问题
  1. 线程安全
  2. 效率
  3. 抵御反射攻击
  4. 序列化

参考博客

为什么要用枚举实现单例模式(避免反射、序列化问题)