摘要
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;
}
}
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();
# 2.3 懒汉式—线程安全
只需要对 getUniqueInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了多次实例化 uniqueInstance 的问题。
但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,因此性能上有一定的损耗。
public static synchronized Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
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;
}
}
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();
}
}
}
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;
}
}
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());
}
}
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;
}
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2.8 枚举实现
这是单例模式的最佳实践,它实现简单,并且在面对复杂的序列化或者反射攻击的时候,能够防止实例化多次。
public enum Singleton {
INSTANCE;
}
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
}
}
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
}
}
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);
}
}
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;
}
... ...
}
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);
}
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");
...
}
...
}
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
结果是:true
。因此:枚举类型对序列化、反序列也是安全的。
综上,可以得出结论:枚举是实现单例模式的最佳实践。毕竟使用它全都是优点:
- 反射安全
- 序列化/反序列化安全
- 写法简单
- 没有一个更有信服力的原因不去使用枚举
# 三:命名的建议
一般建议单例模式的方法命名为:getInstance(),这个方法的返回类型肯定是单例类的类型了。getInstance方法可以有参数,这些参数可能是创建类实例所需要的参数,当然,大多数情况下是不需要的。
# 四:使用场景
- Logger Classes
- Configuration Classes
- Accesing resources in shared mode
- Factories implemented as Singletons
# 五:JDK
- java.lang.Runtime#getRuntime() (opens new window)
- java.awt.Desktop#getDesktop() (opens new window)
- java.lang.System#getSecurityManager() (opens new window)