创建型-单例模式(Singleton)

3/11/2022 设计模式

摘要

JDK:1.8.0_202

# 一:类图

使用一个私有静态变量、一个私有构造函数以及一个公有静态函数来实现。

私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。

类图

# 二:实现方式

实现方式

# 2.1 懒汉式—线程不安全

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }

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

上面实现中,私有静态变量 uniqueInstance 被延迟实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。

这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 if (uniqueInstance == null) ,并且此时 uniqueInstance 为 null,那么会有多个线程执行 uniqueInstance = new Singleton(); 语句,这将导致多次实例化 uniqueInstance。

# 2.2 饿汉式—线程安全

线程不安全问题主要是由于 uniqueInstance 被多次实例化,采取直接实例化 uniqueInstance 的方式就不会产生线程不安全问题。

但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。

private static Singleton uniqueInstance = new Singleton();
1

# 2.3 懒汉式—线程安全

只需要对 getUniqueInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了多次实例化 uniqueInstance 的问题。

但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,因此性能上有一定的损耗

public static synchronized Singleton getUniqueInstance() {
    if (uniqueInstance == null) {
        uniqueInstance = new Singleton();
    }
    return uniqueInstance;
}
1
2
3
4
5
6

# 2.4 双重校验锁—线程安全

使用DCL(Double-Check Locking,双检查锁)机制来实现多线程环境中的延迟加载单例模式。uniqueInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 uniqueInstance 没有被实例化时,才需要进行加锁。

双重校验锁先判断 uniqueInstance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

考虑下面的实现,也就是只使用了一个 if 语句。在 uniqueInstance == null 的情况下,如果两个线程同时执行 if 语句,那么两个线程就会同时进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 uniqueInstance = new Singleton(); 这条语句,只是先后的问题,那么就会进行两次实例化,从而产生了两个实例。因此必须使用双重校验锁,也就是需要使用两个 if 语句。

uniqueInstance 采用 volatile 关键字修饰也是很有必要的。uniqueInstance = new Singleton(); 这段代码其实是分为三步执行。

  • memory = allocate(); // 分配对象内存空间
  • ctorInstance(memory); // 初始化对象
  • uniqueInstance = memory; // 将 uniqueInstance 指向刚分配的内存地址

但是由于 JVM 具有指令重排的特性,JIT 编辑器有可能步骤重排变为了 1>3>2。这时就会出现以下情况:虽然构造方法还没有执行,但 uniqueInstance 具有了内存地址,值不是null,当访问 uniqueInstance 中的实量时还是数据类型的默认值。这在单线程情况下自然是没有问题。但如果是多线程下,有可能获得是一个还没有被初始化的实例,以致于程序出错。可以通过以下代码进行验证。

public class Singleton {

    private static Singleton uniqueInstance;

    public int hasState = 0;

    private Singleton() {
        hasState = new Random().nextInt(200) + 1;
    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }

    public static void reset() {
        uniqueInstance = null;
    }
}


public class Test {

    public static void main(String[] args) throws InterruptedException {
        for (; ; ) {
            CountDownLatch latch = new CountDownLatch(1);
            CountDownLatch end = new CountDownLatch(100);
            for (int i = 0; i < 100; i++) {
                Thread t1 = new Thread(() -> {
                    try {
                        latch.await();
                        Singleton one = Singleton.getUniqueInstance();
                        if (one.hasState == 0) {
                            System.out.println("one.i_am_has_state == 0 进程结束");
                            System.exit(0);
                        }
                        end.countDown();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
                t1.start();
            }
            latch.countDown();
            end.await();
            Singleton.reset();
        }
    }

}
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

根据控制台的输出情况,说明了确实发生了重排序而出现的错误。

综上,使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。同时也让该变量在多个线程间达到可见性。

# 2.5 静态内部类实现

当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getUniqueInstance() 方法从而触发 SingletonHolder.INSTANCE 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例。

这种方式不仅具有延迟初始化的好处,而且由虚拟机提供了对线程安全的支持。

public class Singleton {

    private Singleton() {
    }

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

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

# 2.6 序列化与反序列化的单例模式实现

当将单例的对象进行序列化时,使用默认的反序列行为取出的对象是多例的。

public class Userinfo {
}


public class Singleton implements Serializable {

    private static final long serialVersionUID = 888L;

    public static Userinfo userInfo = new Userinfo();

    private static Singleton singleton = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return singleton;
    }

}

public class Test {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Singleton singleton = Singleton.getInstance();
        System.out.println("序列化-singleton=" + singleton.hashCode() + ",userInfo=" + singleton.userInfo.hashCode());
        // 序列化
        ByteArrayOutputStream byOut = new ByteArrayOutputStream();
        ObjectOutputStream outputStream = new ObjectOutputStream(byOut);
        outputStream.writeObject(singleton);

        // 反序列化
        ByteArrayInputStream byIn = new ByteArrayInputStream(byOut.toByteArray());
        ObjectInputStream inputStream = new ObjectInputStream(byIn);
        Singleton s2 = (Singleton) inputStream.readObject();
        System.out.println("反序列化-singleton=" + s2.hashCode() + ",userInfo=" + s2.userInfo.hashCode());
    }

}
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
37
38
39

从程序运行结果可以分析,在反序列化时创建了新的Singleton对象,内存中产生了两个Singleton对象并不是单例,但Userinfo对象得到复用,因为hashcode是同一个1163157884,为了实现Singleton在内存中一直呈单例的效果,解决办法就是在反序列化时使用readResolve()方法,对原有的Singleton对象进行复用。仅在 Singleton 类中新增下面方法:

protected Object readResolve() throws ObjectStreamException {
    System.out.println("调用了readResolve方法!");
    return Singleton.singleton;
}
1
2
3
4

protected Object readResolve() 方法的作用是在反序列化时不创建新的Singleton对象,而是复用JVM内存中原有的Singleton单例对象,Userinfo对象被复用,也就实现了对Singleton序列化与反序列化时保持单例性的效果。

如果将序列化和反序列化操作分别放入两个class中,则反序列化时会产生新的Singleton对象,放在两个class类中分别执行其实相当于创建了两个JVM虚拟机,每个虚拟机里面的确只有一个Singleton对象,我们想要实现的是在一个JVM虚拟机中进行序列化与反序列化时保持Singleton单例性的效果,而不是创建两个JVM虚拟机。

# 2.7 static 代码块实现单例模式

静态代码块中的代码在使用类的时候就已经执行,所以可以应用静态代码块的这个特性来实现单例模式。

public class Singleton {

    private static Singleton singleton = null;

    private Singleton() {
    }
    
    static {
		singleton = new Singleton();
    }

    public static Singleton getInstance() {
        return singleton;
    }

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

# 2.8 枚举实现

这是单例模式的最佳实践,它实现简单,并且在面对复杂的序列化或者反射攻击的时候,能够防止实例化多次。

public enum Singleton {
    INSTANCE;  
}
1
2
3

前几种方式,有个比较明显的共性,构造器私有化,其实私有化构造器并不安全。因为它抵御不了反射攻击,比如如下下示例代码,典型的 饿汉式

class Singleton implements Serializable {

    private final static Singleton instance = new Singleton();

    private Singleton() {
    }

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

public class Main {
    public static void main(String[] args) throws Exception {
        Singleton s = Singleton.getInstance();

        // 拿到所有的构造函数,包括非public的
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        // 使用空构造函数new一个实例。即使它是private的
        Singleton sReflection = constructor.newInstance();

        System.out.println(s); //com.ccjjltx.creational.singleton.Singleton@1b6d3586
        System.out.println(sReflection); //com.ccjjltx.creational.singleton.Singleton@4554617c
        System.out.println(s == sReflection); // 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
27

再来看序列化问题

public class Main {
    public static void main(String[] args) throws Exception {
        Singleton s1 = Singleton.getInstance();

        ByteArrayOutputStream byOut = new ByteArrayOutputStream();
        ObjectOutputStream outputStream = new ObjectOutputStream(byOut);
        outputStream.writeObject(s1);

        ByteArrayInputStream byIn = new ByteArrayInputStream(byOut.toByteArray());
        ObjectInputStream inputStream = new ObjectInputStream(byIn);
        Object s2 = inputStream.readObject();


        System.out.println(s1); // com.ccjjltx.creational.singleton.Singleton@14ae5a5
        System.out.println(s2); // com.ccjjltx.creational.singleton.Singleton@6d03e736
        System.out.println(s1 == s2);   // false
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

可以看出,序列化前后两个对象并不相等。所以它序列化也是不安全的

下面看看枚举是否能反射攻击:

enum Singleton implements Serializable {
    INSTANCE;
}

public class Main {
    public static void main(String[] args) throws Exception {
        Singleton s = Singleton.INSTANCE;

        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton sReflection = constructor.newInstance();

        System.out.println(s);
        System.out.println(sReflection);
        System.out.println(s == sReflection);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

结果运行就报错:

报错

这个看起来是因为没有空的构造函数导致的,还并不能下定义说防御了反射攻击。那它有什么构造函数呢,可以看它的父类Enum类:

package java.lang;

// 它是所有Enum类的父类,是个抽象类
public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {

	// 唯一的构造函数。程序员不能调用这个构造函数。它供编译器为响应枚举类型声明而发出的代码使用。
	protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
    
	... ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面说明中有说,程序员不得使用该构造器,不过我们可以试试,使用后有什么结果:

public static void main(String[] args) throws Exception {
    Singleton s = Singleton.INSTANCE;

    // 拿到有参构造器
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(String.class, int.class);
    constructor.setAccessible(true);
    System.out.println("拿到了构造器:" + constructor);
    Singleton sReflection = constructor.newInstance("testInstance", 1);

    System.out.println(s);
    System.out.println(sReflection);
    System.out.println(s == sReflection);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

报错

第一句输出了,表示我们是成功拿到了构造器Constructor对象的,只是在执行newInstance时候报错了。并且也提示报错在Constructor的417行,看看Constructor的源码处:

public final class Constructor<T> extends Executable {
	...
    public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		...
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
		...
	}
	...
}
1
2
3
4
5
6
7
8
9
10

主要是这一句:(clazz.getModifiers() & Modifier.ENUM) != 0。说明:反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败,因此枚举类型对反射是绝对安全的。

那么,枚举对序列化、反序列化是否安全?

public static void main(String[] args) throws Exception {
    Singleton s1 = Singleton.INSTANCE;

    ByteArrayOutputStream byOut = new ByteArrayOutputStream();
    ObjectOutputStream outputStream = new ObjectOutputStream(byOut);
    outputStream.writeObject(s1);

    ByteArrayInputStream byIn = new ByteArrayInputStream(byOut.toByteArray());
    ObjectInputStream inputStream = new ObjectInputStream(byIn);
    Object s2 = inputStream.readObject();


    System.out.println(s1); // INSTANCE
    System.out.println(s2); // INSTANCE
    System.out.println(s1 == s2);   // true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

结果是:true。因此:枚举类型对序列化、反序列也是安全的。

综上,可以得出结论:枚举是实现单例模式的最佳实践。毕竟使用它全都是优点:

  1. 反射安全
  2. 序列化/反序列化安全
  3. 写法简单
  4. 没有一个更有信服力的原因不去使用枚举

# 三:命名的建议

一般建议单例模式的方法命名为:getInstance(),这个方法的返回类型肯定是单例类的类型了。getInstance方法可以有参数,这些参数可能是创建类实例所需要的参数,当然,大多数情况下是不需要的。

# 四:使用场景

  • Logger Classes
  • Configuration Classes
  • Accesing resources in shared mode
  • Factories implemented as Singletons

# 五:JDK

# 六:参考文献

最后更新: 9/25/2022, 4:35:24 PM