定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式多种写法
饿汉模式
这种方式在类加载时就完成了初始化,所以类加载比较慢,但是获取对象的速度快。这种方式基于类加载机制避免了多线程的同步问题。1
2
3
4
5
6
7
8public 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
13public 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
13public 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一个新的对象其实是分了三步执行:
- 为该对象分配内存空间
- 初始化对象
- 将对象指向分配的内存地址
由于JVM指令重排的特性,执行顺序可能会变成1->3->2;在单线程环境下不会有问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。比如:
- 线程1执行了1、3
- 线程2调用方法,发现该实例不为空,因此返回该实例。但此时实例还没被初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public 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
12public 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
5public enum Singleton {
INSTANCE;
public void doSomeThing() {
}
}
使用容器实现单例模式
用SingletonManager 将多种的单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。1
2
3
4
5
6
7
8
9
10
11
12
13public 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) ;
}
}