单例模式
定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点
六种写法
1.饿汉式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInatance(){
return instance;
}
}
**饿汉式是典型的空间换时间,在类装载时进行了对象实例化,不管是否使用都先创建出来,类装载较慢,但提取对象的速度快,饿汉式基于JVM类装载的机制避免了多线程同步问题,但是没有达到懒加载的效果, 如果从始至终从未使用过这个实例,则会造成内存的浪费 **
2.懒汉式(线程不安全)
public class Singleton {
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式实例化时机是在第一次调用时,实现Lazy Loading,第一次调用反应稍慢,而且多线程时不安全
懒汉式(线程安全)
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种写法实现了线程安全,但是每次提取实例对象的时候,都需要进行同步,造成不必要的开销,而且大部分时候我们用不到同步,所以不建议使用这种写法
3.双重检测锁(DCL)
public class Singleton {
private volatile static Singleton instance = null;
private Singleton (){
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instanc e = new Singleton();
}
}
}
return instance;
}
}
双重检测锁(Double Checked Locking),有两次对instance的判空,一次在同步块外,次在同步块内,因为有可能多个线程一起进入同步块外的if,如果在同步块内不进行二次检验的话就会生成多个实例了,两次判空也减少了不必要的同步
注意: 这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()
这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错
我们只需要将 instance 变量声明成 volatile 就可以了
4.静态内部类(static nested class)
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
**这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,而第一次加载Singleton类时并不会初始化INSTANCE,只有第一次调用getInstance方法时虚拟机加载SingletonHolder 并初始化INSTANCE,因此它是懒汉式的,这样不仅能确保线程安全也能保证Singleton类的唯一性, 同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。 所以推荐使用静态内部类单例模式 **
5.枚举(Enum)
public enum Singleton{
INSTANCE;
}
可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,不熟悉还有可读性并不是很高
6.容器
public class SingletonManager {
private static Map<String,Object> map=new HashMap<String, Object>();
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);
}
}
用SingletonManager 将多种的单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度
总结
一般来说,单例模式有六种写法:饿汉式 、 懒汉式 、 双重检测锁 、 静态内部 、 枚举 、 容器。
**在开发中,一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy loading)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例 **