单例模式

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式多种写法

饿汉模式

这种方式在类加载时就完成了初始化,所以类加载比较慢,但是获取对象的速度快。这种方式基于类加载机制避免了多线程的同步问题。

1
2
3
4
5
6
7
8
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){}

public static Singleton getSingleton() {
return singleton;
}
}

懒汉模式(线程不安全)

懒汉模式声明了一个静态对象,在用户第一次调用时进行初始化,虽然节约了资源,但第一次加载时需要实例化,反应稍微慢一点,而且在多线程下不能工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton{
private static Singleton singleton;

public Singleton() {
}

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

懒汉模式(线程安全)

这种写法能够在多线程中很好的工作,但是每次调用getSingleton时都需要同步,造成不必要的同步开销,而且大部分时候我们都是用不到同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton{
private static Singleton singleton;

public Singleton() {
}

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

双重检查模式(DCL)

这种写法对singleton进行了两次的判空操作,第一次是为了不必要的同步,第二次是在singleton等于null的情况下才创建实例。这里用到了volatile关键字,或多或少还是会影响性能,但是考虑到程序的正确性,牺牲这点性能还是值得的。

PS:这里为什么要用volatile关键字?

这个地方使用volatile关键字是很有必要的,因为new一个新的对象其实是分了三步执行:

  1. 为该对象分配内存空间
  2. 初始化对象
  3. 将对象指向分配的内存地址

由于JVM指令重排的特性,执行顺序可能会变成1->3->2;在单线程环境下不会有问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。比如:

  1. 线程1执行了1、3
  2. 线程2调用方法,发现该实例不为空,因此返回该实例。但此时实例还没被初始化。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Singleton{
    private volatile static Singleton singleton;

    public Singleton() {
    }

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

静态内部类单例模式

第一次加载Singleton类时并不会初始化sInstance,只有第一次调用getInstance方法时虚拟机加载SingletonHolder 并初始化sInstance ,这样不仅能确保线程安全也能保证Singleton类的唯一性,所以推荐使用静态内部类单例模式。

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton{
public Singleton() {
}

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

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

枚举单例

默认枚举实例的创建是线程安全的,并且在任何情况下都是单例,上述讲的几种单例模式实现中,有一种情况下他们会重新创建对象,那就是反序列化,将一个单例实例对象写到磁盘再读回来,从而获得了一个实例。

枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高,不建议用。

1
2
3
4
5
public enum Singleton {  
INSTANCE;
public void doSomeThing() {
}
}

使用容器实现单例模式

用SingletonManager 将多种的单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SingletonManager { 
  private static Map<String, Object> objMap = new HashMap<String,Object>();
  private Singleton() {
  }
  public static void registerService(String key, Objectinstance) {
    if (!objMap.containsKey(key) ) {
      objMap.put(key, instance) ;
    }
  }
  public static ObjectgetService(String key) {
    return objMap.get(key) ;
  }
}