设计模式-----单例模式

Posted messixiaomo3334

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式-----单例模式相关的知识,希望对你有一定的参考价值。

单例模式

定义

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

六种写法

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 件事情

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将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)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例

以上是关于设计模式-----单例模式的主要内容,如果未能解决你的问题,请参考以下文章

常用代码片段

性能比较好的单例写法

你熟悉的设计模式都有哪些?写出单例模式的实现代码

单例模式以及静态代码块

设计模式之单例模式

设计模式之单例模式