单例对象的类必须保证只有一个实例存在。
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例
确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种型号的对象只应该有且只有一个。
-
构造函数不对外开放,一般为private
-
通过一个静态方法或者枚举返回单例类对象
-
确保单例类的对象有且只有一个,尤其是在多线程环境下
-
确保单例类对象在反序列化时不会重新构建对象
public class Singleton1 {
private static Singleton1 sInstance = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance() {
return sInstance;
}
}
优点
- 线程安全、绝对单例
缺点
-
会浪费内存
-
无法防止反序列化以及反射产生新的实例
public class Singleton2 {
private static Singleton2 sInstance;
private Singleton2() {
}
public static Singleton2 getInstance() {
if (sInstance == null) {
sInstance = new Singleton2();
}
return sInstance;
}
}
优点
- 使用时才加载,节省内存
缺点
-
线程不安全:在多线程情况下,可能出现多个线程同时实例化对象
-
无法防止反序列化以及反射产生新的实例
public class Singleton3 {
private static Singleton3 sInstance;
private Singleton3() {
}
public static Singleton3 getInstance() {
if (sInstance == null) {
synchronized (Singleton3.class) {
if (sInstance == null) {
sInstance = new Singleton3();
}
}
}
return sInstance;
}
}
第一个if语句的作用是避免不必要的同步,第二if语句的作用是在null的情况下创建实例。
优点
- 资源利用率高,大部分情况下线程安全
缺点
-
线程并非完全安全
-
无法防止反序列化以及反射产生新的实例
-
代码丑陋
线程不安全的原因:
sInstance = new Singleton()
这个语句并不是一个原子操作,最终会被编译成多条汇编指令,大致做了三件事:
-
给Singleton的实例分配内存
-
调用Singleton()的构造函数,初始化成员字段
-
将sInstance对象指向分配的内存空间(此时sInstance就不是null了)
但是,由于Java编译器允许处理器乱序执行,以及JDK1.5之前JMM(Java Memory Model,即Java内存模型)中Cache、寄存器到主内存会写顺序的规定,上面的2、3的顺序是无法保证的。也就是说执行顺序可能是1-2-3,也可能是1-3-2。这个时候DCL就会失效,有可能获取到没有初始化的对象。
只需要将sInstance的定义改为如下这样:
private volatile static Singleton3 sInstance;
这样就可以保证sInstance每次都是从主内存中读取。(关于volatile的详细用法请看这篇文章)
但是,volatile会影响性能。
public class Singleton4 {
private Singleton4() {
}
public static Singleton4 getInstance() {
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static final Singleton4 sInstance = new Singleton4();
}
}
当第一次加载Singleton类时并不会初始化sInstance,只有第一次调用Singleton.getInstance()方法时,才会导致sInstance被初始化。 因此,第一次调用getInstance()会导致虚拟机加载SingletonHolder类,这种方式不仅能保证线程安全,也能保证单例对象的唯一性,同时也延迟了单例的实例化。
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
优点
- 线程安全
线程安全原因:Java的运行机制保证了static修饰的类、对象仅被初始化一次。
缺点
- 无法防止反序列化以及反射产生新的实例
public enum Singleton5 {
INSTANCE;
}
优点
-
简单
-
线程安全
-
防止反序列化、反射产生新的对象
缺点
- 内存浪费(与饿汉模式类似)
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<String, Object>();
private SingletonManager() {
}
public static void registerService(String key, Object instance) {
if (!objMap.containsKey(key)) {
objMap.put(key, instance);
}
}
public static Object getInstance(String key) {
return objMap.get(key);
}
}
在程序的初始,将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对象对应类型的对象。 这种方式使得可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作。