0%

单例模式

什么是单例模式?

单例模式算是设计模式入门的最简单的一个模式, 由于 Java 语言的特性(指令重排序), 导致同时也是最难的一个模式。

所幸, 先驱者Joshua BlochGoogle I/O 2008上的新书Effective Java介绍了单例模式的最佳实践。这本神书我还没来的及看, 但在国外好像备受推崇, 所以有机会还是看看。

一个android应用程序的一个单例模式的类只能有且只有一个实例对象。
通俗的讲,就是构造方法私有化,并在类内创建唯一一个私有的类实例,提供一个用于获取唯一实例的公有方法。

1
2
3
4
5
6
7
8
public class A {
private static A instance; //创建唯一一个私有实例
private A() {} //私有化构造函数
public static A getInstance() {
instance = new A(); //初始化
return instance; //返回唯一实例
}
}

单例模式分为以下两种类型

  1. 饿汉模式:类加载的时候便进行创建(加载类时较慢,运行时获取对象较快,线程安全)
  2. 懒汉模式:要使用的时候才进行创建(加载类时较快,运行时获取对象较慢,线程不安全)

饿汉模式

在Java 5 之前的最佳实践

直接在声明的时候初始化, 当类加载时, instance = new A()就会执行.

1
2
3
4
5
6
7
public class A {
private static A instance = new A(); //注意这里初始化的时机
private A() {} //私有化构造函数
public static A getInstance() {
return instance; //返回唯一实例
}
}

Java 5 之后的最佳实践

enum 作为枚举关键字, 很难想像到它和 单例模式 联系到一起。
但是换种思路, 枚举类只有一个元素, 不就是单例模式了吗?
枚举类还可以添加自定义的方法。完全可以当成一个类来使用。

1
2
3
4
public enum A {
INSTANCE;
public static A getInstance() { return INSTANCE; }
}

懒汉模式

要使用的时候才进行创建

静态内部类 (在Java 5 之前的最佳实践)

因为Holder是静态内部类, 只有getInstance()方法访问到Holder, 才会初始化静态内部类的static的变量.

1
2
3
4
5
6
7
8
9
public class A {  
private static class Holder {
private static final A instance = new A();
}
private A (){}
public static final A getInstance() {
return Holder.instance;
}
}

双重锁+volatile (在Java 5 之后的最佳实践)

双重锁的优化

上面提到,懒汉式是线程不安全的,对于多线程比较陌生的可能不太理解,先看代码

1
2
3
4
5
6
7
8
9
10
public class A {
private static A instance;
private A() {}
public static A getInstance() {
if(instance == null) {// 一
instance = new A();// 二
}
return instance;// 三
}
}

当我们获取调用getInstance()方法获取A的实例对象时,如果有多个线程调用getInstance()方法,就会出现线程不安全,怎么解释呢?

假设有两个线程同时调用了getInstance()方法.

  1. 当线程1执行到if(instance == null)时, 判断为true, 进入if内, 此时线程1时间片结束, CPU切换到线程2.
  2. 此时线程2执行到if(instance == null), 因为线程1还未来得及执行instance = A(), 所以if判断为true, 进入方法体, 此时线程2时间片结束, CPU切换到线程1.
  3. 此时线程1执行instance = A(), instancehashcode850, 然后return, 执行结束, 切换回线程2.
  4. 此时线程2在if内, 执行instance = A(), instancehashcode851, 然后return.

看到了吗? 这样就创建了两个A的实例. 说好的单例模式呢?

那么改一下, 加上synchronized同步一下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class A{
private static A instance;
private A() {}
public static A getInstance() {
if(instance == null) { // 一
synchronized (A.class) { // 二
if(instance == null) { // 三
instance = new A(); // 四
}
}
}
return instance; // 五
}
}

假设有两个线程同时调用了getInstance()方法.

  1. 当线程1执行到if(instance == null)时, 判断为true, 进入if内, 此时线程1时间片结束, CPU切换到线程2.
  2. 此时线程2执行到if(instance == null), 因为线程1还未来得及执行instance = A(), 所以if判断为true, 进入方法体, 此时线程2时间片结束, CPU切换到线程1.
  3. 线程1拿到A.class锁, 进入synchronized, 此时就算切换回线程2, 线程2也会因为拿不到A.class锁对象, 阻塞在同步代码块外面.
  4. 线程1继续执行if, 执行instance = A(), instancehashcode850, 然后return, 执行结束, 切换回线程2.
  5. 此时线程2拿到A.class锁, 进入synchronized, if判断为false, 直接return.

为什么不把第一个if去掉, 直接留个synchronized + if呢?
因为synchronized耗时长, 消耗性能, 双重锁只要保证第一次并发不产生多个对象即可.

你以为这样就线程安全了吗! Java还有个指令重排序的大招等着你呢.

volatile 优化

volatile关键字用来解决指令重排序在多线程下的问题, 它有两个功能.

  1. 保证内存可见性
  2. 防止指令重排序

我们先看内存可见性(其实这已经被synchronized解决了)
Java内存模型规定, 变量存储在主存中, 每个线程拥有该变量的一个拷贝副本在自己的工作内存中, 线程修改变量是修改自己工作内存中的变量, 而修改完毕后, 会将自己工作内存中的修改后的值回写到主存中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class A {
private static A instance;
private A() {}
public static A getInstance() {
if(instance == null) { // 一
synchronized (A.class) { // 二
if(instance == null) { // 三
instance = new A(); // 四
}
}
}
return instance; // 五
}
}

假设有两个线程同时调用了getInstance()方法.

  1. 当线程1执行到if(instance == null)时, 判断为true, 进入if内, 拿到A.class锁, 进入synchronized, 继续执行if, 执行instance = A(), instancehashcode850, 此时instance没有回写到主存, 切换回线程2.
  2. 线程2执行到第一个if, 因为线程1的值没有回写到主存, 所以还是会进入if内, 但是被synchronized阻塞了.
  3. 线程1将变量回写到主存, 并return.
  4. 此时线程2拿到A.class锁, 进入synchronized, if判断为false, 直接return.
    所以就算没有使用volatile保证内存可见性, 也不会导致出错.
    其实synchronized就有保证内存可见性的功能.

    java.util.concurrent.locks.Lock 接口的Javadoc中有这样一段话:
    All Lock implementations must enforce the same memory synchronization semantics as provided by the built-in monitor lock

再来看指令重排序
instance = A(); 这条命令并不是原子性的, 它包含3个操作.

  1. 在堆内存申请A实例的内存空间.
  2. 初始化A实例.
  3. instance变量指向内存中的A实例的内存空间(执行完这步 instance 就为非 null 了)
    由于JVM的指令重排序, 步骤123可能会变成132, 多线程下会导致出错.

假设有两个线程同时调用了getInstance()方法. 指令顺序为132.

  1. 当线程1执行到if(instance == null)时, 判断为true, 进入if内, 拿到A.class锁, 进入synchronized, 继续执行if, 执行instance = A().
  2. 先在堆内存申请A实例的内存空间, 由于指令重排序, 将instance变量指向没有初始化的, 但是已经申请了的内存空间. 此时线程1时间片结束, CPU切换到线程2.
  3. 线程2执行到第一个if, 因为instance已经指向没有初始化的内存空间, 所以直接return. 这时调用这个单例, 因为还未初始化, 所以会导致错误出现.

volatile可以防止指令重排序, 让指令严格按照123的顺序执行.
但是, 在Java 5之前, 因为Java内存模型的缺陷, volatile不能解决指令重排序的问题.
所以最佳实践就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class A {
private volatile static A instance;
private A() {}
public static A getInstance() {
if(instance == null) {
synchronized (A.class) {
if(instance == null) {
instance = new A();
}
}
}
return instance;
}
}

破坏单例模式的攻击

克隆 clone 攻击

要用clone攻击单例模式只需要两步.

  1. A类实现Cloneable接口, 虽然里面啥也没有, 主要时为了解决CloneNotSupportedException.
  2. 重写Object中的protected native Object clone()方法, 不然外部访问不了.
    然后运行main方法, a1==a2得到false.
    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
    public class A implements Cloneable {
    private volatile static A instance;
    private A() {}
    public static A getInstance() {
    if(instance == null) {
    synchronized (A.class) {
    if(instance == null) {
    instance = new A();
    }
    }
    }
    return instance;
    }

    public Object clone() throws CloneNotSupportedException {
    return (A) super.clone();
    }
    }

    public class Main {
    public static void main(String[] args) throws Exception {
    A a1 = A.getInstance();
    A a2 = (A) a1.clone();
    System.out.println(a1 == a2); // false
    }
    }
    要解决clone攻击也很简单, 既然是clone方法的问题, 那我们就直接在clone方法改动即可.
    1
    2
    3
    4
    5
    public class A implements Cloneable {
    public Object clone() throws CloneNotSupportedException {
    return getInstance();
    }
    }

反射攻击

constructor.setAccessible(true);破解私有构造函数.

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
32
33
public class A {
private volatile static A instance;
private A() {}
public static A getInstance() {
if(instance == null) {
synchronized (A.class) {
if(instance == null) {
instance = new A();
}
}
}
return instance;
}
}

public class Main {
public static void main(String[] args) throws Exception {
A a1 = A.getInstance();
A a2 = create(A.class);
System.out.println(a1 == a2); // false
}

public static <T> T create(Class<T> clazz) {
try {
Constructor<T> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
} catch (InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}

那我们只能在构造函数体做文章了.
判断如果被初始化过了, 再次初始化则抛出异常即可.

1
2
3
4
5
6
7
8
9
10
11
12
public class A {
private static A instance; //创建唯一一个私有实例
private A() { //私有化构造函数
if(instance != null) { //避免反射创建
throw new IllegalStateException("单例模式不允许再创建");
}
}
public static A getInstance() {
instance = new A(); //初始化
return instance; //返回唯一实例
}
}

序列化攻击

只要对A类实现Serializable接口, 即可进行对象序列化.

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
32
public class A implements Serializable {
private volatile static A instance;
private A() {}
public static A getInstance() {
if(instance == null) {
synchronized (A.class) {
if(instance == null) {
instance = new A();
}
}
}
return instance;
}
}
public class Main {
public static void main(String[] args) throws Exception {
File file = new File("serializable.txt");
A a1 = A.getInstance();
// 1. 序列化
try (FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);) {
oos.writeObject(a1);
oos.flush();
}
// 2. 反序列化
try (FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);) {
A a2 = (A) ois.readObject();
System.out.println(a1 == a2); // false
}
}
}

要化解序列化攻击, 很简单, 我们先看ois.readObject()这个方法.
readObject调用链
最终调用的是readOrdinaryObject方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ObjectInputStream extends InputStream implements ObjectInput, ObjectStreamConstants {
private Object readOrdinaryObject(boolean unshared) throws IOException {
// 省略部分代码
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
// 省略部分代码
Object obj;
try {
// =============== !!注意这里!! ===============
obj = desc.isInstantiable() ? desc.newInstance() : null;
// =============== !!注意这里!! ===============
} catch (Exception ex) {
throw (IOException) new InvalidClassException(desc.forClass().getName(), "unable to create instance").initCause(ex);
}

if (obj != null && handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod()) { // 注意这里, false
// 省略部分代码
}

return obj;
}
}

我们可以看到desc.isInstantiable()返回true之后, 反序列化会通过反射创建一个新的对象.
看下isInstantiable()的文档.

Returns true if represented class is serializable/externalizable and can be instantiated by the serialization runtime–i.e., if it is externalizable and defines a public no-arg constructor, or if it is non-externalizable and its first non-serializable superclass defines an accessible no-arg constructor. Otherwise, returns false.

也就是说, 满足以下两种情况任意一种, 则返回true

  1. 类实现了Externalizable接口, 并定义了一个无参构造器
  2. 类没有实现Externalizable接口, 它的第一个非Serializable父类(如Object)定义了一个无参构造器.

我们继续往下看, 根据方法名, 如果A类实现了readResolve方法, 就会调用readResolve方法, 并返回出去.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ObjectInputStream extends InputStream implements ObjectInput, ObjectStreamConstants {
private Object readOrdinaryObject(boolean unshared) throws IOException {
Object obj;
// 省略部分代码
if (obj != null && handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod()) { // 注意这里, true
Object rep = desc.invokeReadResolve(obj);
// 省略部分代码
if (rep != obj) {
// 省略部分代码
obj = rep;
// 省略部分代码
}
}

return obj;
}
}

所以, 要化解序列化攻击, 只需要写一个readResolve方法.

1
2
3
4
5
public class A implements Serializable {
private Object readResolve() {
return getInstance();
}
}

究极无敌完全进化完美精华牛逼上天单例模式懒汉版

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
32
33
34
35
36
public class A implements Serializable, Cloneable {
private volatile static A instance;
private A() {
if(instance != null) { // 化解反射攻击
throw new IllegalStateException("单例模式不允许再创建");
}
}

/**
* 双重锁保证线程安全
*/
public static A getInstance() {
if(instance == null) {
synchronized (A.class) {
if(instance == null) {
instance = new A();
}
}
}
return instance;
}

/**
* 化解克隆攻击
*/
public Object clone() throws CloneNotSupportedException {
return getInstance();
}

/**
* 化解序列化攻击
*/
private Object readResolve() {
return getInstance();
}
}

参考资料

点击进入云乞讨模式!