单例模式

Wafer Li ... 2017-03-12 《Head First 设计模式》笔记
  • DesignPattern
  • 读书笔记
大约 6 分钟

# 1. 概述

单例模式,顾名思义,它可以保证在系统运行中,整个系统只存在某个类的 一个对象

该对象在系统中是 唯一的

# 2. 经典实现

需要注意的是,虽然这个实现很经典,但是它是错误的,在实际中不应该使用;

但是,由于它较为简单,所以拿这种实现来说明单例的原理。

public class Singleton {
    // 存储自身的引用
    private static Singleton instance = null;

    private Singleton() {
        // 不允许外界通过构造器构建
    }

    private static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

该实现中,最为核心的就是 getInstance() 方法;

通过 if 的判断,如果实例为空,那么进行构造;

当实例构造完毕后,instance 就不为空了,直接返回自身。

这样就保证了在普通情况下,对象只会有一个。

但是,这种实现在多线程时不成立!

当有两个线程同时尝试获取单例实例时,就会造成对象的重复构建:

这是由于两个线程可能同时进入 if 区域中,导致两个线程分别执行对象的构建,最后就会出现两个单例对象。

# 3. 懒汉式

由于经典实现的缺陷是因为多线程导致的;

那么我们只要把 getInstance() 设置为同步方法不就完了吗?

private static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }

    return instance;
}
1
2
3
4
5
6
7

但是,这种实现方法具有很严重的 性能缺陷

由于每次获取单例实例都需要先获取锁,导致性能低下;

而实际上,只需要在单例的构建过程进行同步即可;

没有必要在每次获取对象的时候都进行同步。

# 4. 饿汉式

既然多线程造成的问题是在 getInstance() 时发生的;

而使用 synchronized 关键字又存在性能缺陷;

那么,为什么不在类加载的一开始就进行实例构建呢?

这样,既避免了多线程问题,又没有同步损失。

public class Singleton {

    private Singleton() {

    }

    private static final Singleton instance = new Singleton();

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

这样虽然解决了同步损失,但是,单例的构建很可能是一个耗时操作;

它并不是一个懒加载操作,同时,如果单例的构建需要外部参数的话,这个方法就用不上了。

# 5. 双检锁单例

这种单例实现是目前用的比较多的形式;

既然单例的多线程同步只需要在对象构建时进行,那么,我们就可以通过对 instance 进行两次检查。

private static Singleton getInstance() {
    // 检查对象是否已经构建
    if (instance == null) {
        synchronized(Singleton.class) {
            // 防止两个线程同时进入第一个 if 造成的对象重复构建
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }

    return instance;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

这里着重解释第二个判断:

当两个线程通过第一个 if 进入同步区后;

线程一先获取锁,进行对象构建;

线程一完毕后,线程二进入同步区;

如果此时没有第二个 if,那么对象就会进行重复构建。

当对象构建完毕后,外部的 if 将会跳过,不会再进行同步过程;

这样就解决了同步的性能损失。

看起来这个实现已经很完美了,但是还是有问题;

问题就在于,instance = new Singleton() 不是一个原子操作

这个操作分为三步:

  1. instance 分配内存
  2. 调用 Singleton 的构造函数进行对象构造
  3. instance 指向分配的内存空间

而在 JIT 即时编译优化中,会出现指令重排;

最终的执行顺序很可能不是 1-2-3 而是 1-3-2;

那么在 3 完成后,线程二抢占锁,此时 instance 不为空,于是线程二返回,报错。

一个改进则是对于 instance 变量采用 volatile 进行修饰,防止指令重排;

volatile 变量在赋值操作后会存在 内存屏障,防止读操作在赋值操作之前进行。

所以,双检锁单例的正确实现形式如下:

public class Singleton {
    private static volatile Singleton instance = null;

    private Singleton() {

    }

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

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

# 6. 静态内部类

可以看到,上面的双检锁单例的实现过于繁琐;

有没有一种既线程安全,又采用懒加载而且实现简单的实现方法呢?

有的,我们可以采用静态内部类来实现单例。

public class Singleton {
    private Singleton() {

    }

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

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

首先看到,我们把单例的实例放入了它的静态内部类中;

这种实现方法的难度比双检锁单例下降了许多;

其原理在于,我们不再 自行实现 线程安全;

通过将实例交给静态内部类,我们可以让 JVM 保证天生的线程安全。

Singleton 被加载时,其内部类并不会初始化;

而当 getInstance() 调用时,其内部类被 加载 入内存中;

JVM 保证所有对类的读写操作均在类加载 之后 进行;

这样,就保证了线程安全;

同时,由于 getInstance() 不是同步方法,也不会有同步损失。

# 7. 序列化和反射攻击

那么,我们的单例实现是否已经没有问题了呢?

并不是!

由于我们都是通过设定 访问修饰符 来达到将构造函数封装的目的;

但是,当其他使用者使用 反射 来进行对象构建时,单例模式就会被破坏了!

Constructor<?>[] cons = getDeclaredConstructors();
cons. setAccessible(true);
1
2

其中,一个很常见的问题就是单例的序列化;

当我们采用 Serializable 接口时;

对象在序列化和反序列化的过程中,会使用 反射 调用无参构造方法进行对象构造。

那么,我们该如何防止对象被反射攻击呢?

一种方法是采用 flag,如果对象被反射攻击,那么就抛出异常;

public class Singleton {
    private static boolean flag = false;

    private Singleton() {
        if (Singleton.flag == false) {
            Singleton.flag = true;
        }
        else {
            throw new RuntimeException("Reflect Attack!");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

而对于序列化造成的对象的重复构建,我们可以采用重载 readResolve() 的方法进行。

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

# 8. 枚举实现(Best Practice)

可以看到,当我们把所有问题都考虑到之后,单例的实现已经变得非常复杂了。

所以有没有一种更为简单的方法能满足上面的所有要求呢?

这就是枚举实现:

public enum Singleton {
    INSTANCE;

    public void doSomeThing() {
        // operations
    }
}
1
2
3
4
5
6
7

使用:

public static void main(String[] args) {
    Singleton.INSTANCE.doSomeThing();
}
1
2
3

它满足:

  1. 线程安全
  2. 无性能损失
  3. 防反射攻击
  4. 防止序列化重复构建

而且是最为简单的一种单例实现方法。