单例模式

介绍

许多时候系统只允许拥有一个对象实例,例如一个系统只能有一个ImageLoader,里面又含有线程池,缓存系统,访问网络的操作,等等,比较消耗资源,因此没有理由创建多个实例,这种不能自由创建多个对象的情况,就是单例模式的使用情景。

使用场景

确保某个类的对象只有一个的场景,避免创建多个对象造成过多的内存消耗的情景,或者某种类型的对象只应该有且只有一个。例如创建一个对象消耗的资源过多,如果访问IO和数据库资源这时候就应该考虑使用单例模式。

代码实现:

饿汉模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Singleton {

private static Singleton singleton = new Singleton();

private Singleton(){
//防止反射访问私有的构造方法
if (singleton != null) {
try {
throw new IllegalAccessException("单例对象已经被实例化了,请不要随便使用非法的反射!");
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

public static Singleton getInstance(){
return singleton;
}

}

类加载的时候,就完成了singleton的初始化,是线程安全的,而且静态成员变量只会初始化一次,所以也保证了单例,但是不能够实现延迟加载,导致内存(方法区和堆)的利用率不高。

懒汉模式

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 Singleton {

private static Singleton singleton ;

private Singleton(){
//防止反射访问私有的构造方法
if (singleton != null) {
try {
throw new IllegalAccessException("单例对象已经被实例化了,请不要随便使用非法的反射!");
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

public static synchronized Singleton getInstance(){
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}

}

在类加载过程的准备阶段,就为静态成员变量分配了内存(只不过这里在方法区上,但是堆上还没有创建对象,未占用内存),也就是前面所说的无法实现延迟加载,而且虽然是线程安全的,但是因为每次调用的时候都要进行同步处理,所以效率比较低。

双检锁(DCL double check lock)

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
public class Singleton {

private static Singleton singleton ;

private Singleton(){
//防止反射访问私有的构造方法
if (singleton != null) {
try {
throw new IllegalAccessException("单例对象已经被实例化了,请不要随便使用非法的反射!");
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

public static Singleton getInstance(){
if(singleton==null){

synchronized (Singleton.class){
if(singleton==null){
singleton = new Singleton();
}
}

}
return singleton;
}

}

保证了线程安全,同时只会在一次的使用的时候进行同步操作,效率比较高,但是在类加载过程的准备阶段,就为静态成员变量分配了内存(只不过这里在方法区上,但是堆上还没有创建对象,未占用内存),也就是前面所说的无法实现延迟加载。

第一层判断主要是为了避免在不必要的同步,第二层判断是为了在null的情况下创建实例。

但是这种写法有时候会出现DCL失效的问题。
singleton = new Singleton()被编译成多条的指令,大致做了三件事情:

  • 分配内存
  • 执行构造函数,初始化成员变量
  • 将singleton指向新分配的内存空间

java编译器允许乱序执行,同时由于在JDK 1.5 之前JVM中Cache,寄存器到主内存回写顺序的规定。上面的第二步跟第三步的顺序是无法保证的,也就是说可能会出现,第三步先执行,第二步后执行的情况,当A线程执行了第三步,切换到B线程执行,此时singleton不为空,返回直接返回singleton,使用的时候会出错。这就是DCL失效的问题。

修改,可以利用volatile禁止指令重排序:

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
public class Singleton {

private volatile static Singleton singleton ;

private Singleton(){
//防止反射访问私有的构造方法
if (singleton != null) {
try {
throw new IllegalAccessException("单例对象已经被实例化了,请不要随便使用非法的反射!");
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

public static Singleton getInstance(){
if(singleton==null){

synchronized (Singleton.class){
if(singleton==null){
singleton = new Singleton();
}
}

}
return singleton;
}

}

静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Singleton {

private Singleton(){
//防止反射访问私有的构造方法
if (SingletonHolder.singleton != null) {
try {
throw new IllegalAccessException("单例对象已经被实例化了,请不要随便使用非法的反射!");
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

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

public static Singleton getInstance(){
return SingletonHolder.singleton;
}

}

线程安全的,同时实现了延迟加载,也修复了双检锁模式的问题。
不管内部类是静态的还是非静态的,都不会因为外部类的类加载或者对象创建而去加载、初始化,一般是这样的,也有可能不是,建议阅读:
JAVA内部类有关 
加载一个类时,其内部类是否同时被加载?引申出单例模式的另一种实现方式
将SingletonHolder改为非static报错,建议阅读:
非静态的Java Inner Class 为什么不能有static方法?
补充阅读:
从一道面试题来认识java类加载时机与过程
Java中静态(static)成员何时才会初始化

枚举单例

1
2
3
4
5
6
7
8
9
public enum  Singleton {

SINGLETON;

public void doSometing(){
System.out.println("wo shi mei ju lei!!!!");
}

}

枚举类,写法简单,而且默认情况下是线程安全的,并且在任何情况下他都是一个单例。上述的几种写法,在反序列化的时候,会出现重新创建对象实例的情况。但是枚举类的写法却不会。

然而枚举也存在延迟加载的问题,只要加载无论是否使用单例,都会占用内存,但是枚举的构造函数通过反射获取到以后再newInstance是非法的(见例一),因此枚举实现的单例无需在私有的构造函数中再进行单例的判断从而控制构造函数被非法反射调用,即在私有构造函数中省略了if(instance != null){抛异常}。

一般单例类都是作为工具类来使用,不需要序列化,因此不需要实现java.io.Serializable接口;特殊情况下,如果单例类实现了序列化接口,只需要再readResolve方法中返回单例即可。建议阅读:readResolve()方法与序列化

例:

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 SingleInstance implements Serializable{

private SingleInstance() {
if (SingleInstanceHolder.INSTANCE != null) {
try {
throw new IllegalAccessException("单例对象已经被实例化了,请不要随便使用非法的反射!");
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}


private static class SingleInstanceHolder {
private static final SingleInstance INSTANCE = new SingleInstance();
}

public static SingleInstance getInstane() {
return SingleInstanceHolder.INSTANCE;
}

//保证反序列化的时候,仍然不会创建新的对象,保证了单例模式。
private Object readResolve() throws ObjectStreamException {
return SingleInstanceHolder.INSTANCE;
}

}

使用容器实现单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class  SingletonManager {

private static Map<String,Object> map = new Hashtable<>();

private SingletonManager(){

}

public static void registerService(String key,Object instance){
if(!map.containsKey(key)){
map.put(key,instance);
}
}

public static Object getService(String key){
return map.get(key);
}
}

在程序初始化的时候,将各种单例类放进一个统一个的管理类中进行管理,需要的时候通过key取出对应的单例。这种方式可以使我们管理多种类型的单例。同时使用同一的接口进行管理,降低了用户的使用的成本,同时在使用的过程中隐藏了内部的细节,降低了耦合性。

总结:

不论用那种方式实现单例,核心思想都是将构造函数私有化,同时通过一个静态的方法,获取唯一的一个实例。在获取的过程中必须保证线程安全的,同时防止在反序列化的过程中重新生成类的实例的情况。

建议阅读:【J2SE】为什么静态内部类的单例可以实现延迟加载

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器