单例模式,从定义上看起来很简单,但是在实现上是富有争议的, 他严格规定了对象实例化的个数,而且保证在运行期间,JVM只存在一个实例。

在实现上,程序必须提供接口去访问该唯一实例。在运用上,单例模式可以运用在 logging, driver objects, caching 和 thread pool

在其他模式的实现中,也运用了单例模式,比如抽象工厂模式,生成器模式,原型模式,门面模式等。在java标准库中,java.lang.Runtime, java.awt.Desktop也运用了单例模式。

下面介绍常用的单例实现方法,和其使用方法

首先,所有的实现方法必须遵循以下几个原则:

  1. 私有构造函数,保证该类不能从其他类中进行实例化
  2. 私有静态类实例,保证该类只有一份实例
  3. 共有的静态函数,返回该类的唯一的实例变量,这也是唯一一个外界获取该类实力对象的方法

饿汉实现方式

该种实现方式中,类实例是当类加载时就创建了,这种方式也是最简单的,但是这个缺点,就是该实例并不是在使用的时候才创建
下面是该种方式的实现代码

1
2
3
4
5
6
7
8
9
10
11
12
package com.walterlife.designparttern;

public class Singleton {
private static final Singleton instance = new Singleton();

private Singleton() {
}

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

如果程序中你不使用什么资源,该种方式是一种好的选择。但是在大部分场景中,单例中都会包含资源了,比如database connections, file handler. etc.

静态语句块实例化实现方式

该种实现方式和饿汉实现方式类似,除了该实例是在静态语句块创建的,并且提供了额外的异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.walterlife.designparttern;

public class Singleton {

private static Singleton instance = null;

private Singleton() {
}

static {
try {
if(instance == null) {
instance = new Singleton();
}
} catch(Exception ex) {
throw new RuntimeException("Exception occured in creating singleton instance");
}
}

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

以上两种方式都是在使用对象之前就把对象实例化好,所以该类方法在并不推荐使用,接下来介绍使用懒汉模式实现的单例模式

懒汉实现方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.walterlife.designparttern;

public class Singleton {

private static Singleton instance = null;

private Singleton() {}

public 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
16
package com.walterlife.designparttern;

public class Singleton {

private static Singleton instance = null;

private Singleton() {}

public static synchronized 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
16
17
18
package com.walterlife.designparttern;

public class Singleton {
private static Singleton instance = null;

private Singleton() {}

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

Bill Pugh 单例实现方式

使用static inner class 来存储单例模式,这样就不会在单例类加载的时候就实例化单例对象

1
2
3
4
5
6
7
8
9
10
11
package com.walterlife.designparttern;

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

public static SingletonHelper getInstance() {
return SingletonHelper.INSTANCE;
}
}

上面静态内部类并不会在单例类加载的时候就去加载,而是在调用getInstance() 函数的时候才会将Inner Class 加载至内存。
该种方式也是不需要同步的,所以也比较常用

使用反射机制释放单例内存

以上任何一种实现单例方式都可以使用反射机制释放单例的内存

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
package com.walterlife.dp.SingletonDP;

import java.lang.reflect.Constructor;

// @file ReflectionSingletonTest.java
// @brief
// @author walterzywei@gmail.com
// @version 1.0

public class ReflectionSingletonTest {
public static void main (String [] args) {
Singleton instanceOne = Singleton.getInstance();
Singleton instanceTwo = null;

try {
Constructor[] constructors = Singleton.class.getDeclaredConstructors();

for(Constructor constructor: constructors) {
constructor.setAccessible(true);
instanceTwo = (Singleton)constructor.newInstance();
break;
}
} catch(Exception e) {
e.printStackTrace();
}

System.out.printf("instanceOne hashCode:%d\ninstanceTwo hashCode:%d\n", instanceOne.hashCode(), instanceTwo.hashCode());
}
}

输出结果
instanceOne hashCode:1975012498
instanceTwo hashCode:1808253012

从输出结果看,instanceOne和instanceTwo的hashCode 值不同,说明已经被JVM回收了

使用enum 实现单例模式

为了避免反射机制带来的问题[可以更改属性的可见性], Joshua Bloch提出了使用enum 来实现单例模式。因为enum中属性只初始化一次,且是全局可见的,需要用类名引用

1
2
3
4
5
6
7
8
9
10
package com.walterlife.dp.SingletonDP;

// @file EnumSingleton.java
// @brief
// @author walterzywei@gmail.com
// @version 1.0

public enum EnumSingleton {
INSTANCE;
}

测试代码

1
2
3
4
5
6
public static void testSingletonDP() {

for(EnumSingleton instance: EnumSingleton.values()) {
System.out.printf("Instance: %s\n", instance);
}
}

序列化和单例模式

有时候在分布式系统中,我们需要将对象的状态存储在文件中,用于下次系统启动时恢复对象的状态,此时我们就需要将对象实现Serialized 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.walterlife.dp.SingletonDP;

import java.io.Serializable;

// @file SerializedSingleton.java
// @brief
// @author walterzywei@gmail.com

public class SerializedSingleton implements Serializable {

private static final long serialVersionUID = 6474866740300032131L;

private SerializedSingleton() {}

private static class SingletonHelper {
private static final SerializedSingleton instance = new SerializedSingleton();
}

public static SerializedSingleton getInstance() {
return SingletonHelper.instance;
}
}

测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void testSerializedSingleton() throws FileNotFoundException, IOException, ClassNotFoundException {
SerializedSingleton instanceOne = SerializedSingleton.getInstance();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream("test_serialized"));

out.writeObject(instanceOne);
out.close();

ObjectInput in = new ObjectInputStream(new FileInputStream("test_serialized"));
SerializedSingleton instanceTwo = (SerializedSingleton)in.readObject();
in.close();

System.out.printf("Instance One hashCode: %d\n", instanceOne.hashCode());
System.out.printf("Instance Two hashCode: %d\n", instanceTwo.hashCode());

}

测试输出

1
2
Instance One hashCode: 885284298
Instance Two hashCode: 1389133897

上述输出,两个单例的hashCode 结果竟然不相同,这就违反了单例模式的原则,那怎么解决呢

只要实现 Object 的 readResolve 方法即可

1
2
3
protected Object readResolve() {
return getInstance();
}

输出结果

1
2
Instance One hashCode: 885284298
Instance Two hashCode: 885284298

这样两个单例实例的hashCode 值就相同咯-_-

留言