单例模式是我们应用最广的设计模式,所以对于像我这样的菜鸟一样会接触到很多。在使用这种设计模式的时候,单例对象的类必须保证只有一个实例存在。
单例模式的定义
确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式的适用场景
确保一个类只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。例如创建一个对象要使用的资源过多,或者需要访问IO和数据库等,就建议使用单例模式。
单例模式通用结构图
- Client为客户端
- Singleton是单例类,通过调用Singleton.getInstance()来获取实例对象
实现单例模式的主要关键点:
- 构造函数不对外开放,一般为Private,使得客户端代码不能通过new的形式手动构造单例类的对象。
- 通过一个静态方法或者枚举返回单例类对象
- 确保单例类的对象有且只有一个,尤其是在多线程环境下
- 确保单例类对象在反序列化时不会重新后构建对象
单例模式一共有7种写法
第一种:饿汉模式
public class Singleton{
private static Singleton instance=new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
在类加载的时候就完成了初始化,所以类加载较慢,但获取对象的速度快。这种方式基于类加载机制,避免了多线程的同步问题。
第二种:懒汉模式(线程不安全)
```java
public class Singleton{
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if(insatnce==null){
instance=new Singleton();
}
return instance;
}
}
懒汉模式声明了一个静态对象,在第一次使用的才的才初始化。节约了资源。
第三种:线程安全的懒汉模式
public class Singleton{
private static Singleton instance;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}
对于线程安全的懒汉模式一看就知道发生了什么变化,只不过是在得到instance的时候对其加锁,确实可以实现在多线程的情况下很好的工作,但是在第一次调用的时候需要即使进行初始化,而且每次调用getInstance的时候都需要进行同步,会造成不必要的同步开销。(不建议使用,很少使用同步)
第四种:双重检查模式(DCL)
public class Singleton{
private static volatile Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance==null){//为了不必要的同步
synchronized(Singleton.class){
if(instance==null){
instance=new Singleton();
}
}
}
return instance;
}
}
优点:既能够在需要时才初始化单例,又能后保证线程安全,且单例对象初始化后调用getInstance不进行同步锁。
缺点:第一次加载时反应稍慢,在高并环境下有一定缺陷。
在该程序种设置了两次判空,第一次是为了避免不必要的同步,第二次是为了在null的情况下创建实例如何理解呢?
分析:
在instance=new Singleton()这句代码不是一句原子指令,大概做了三件事:
- 给Singleton的实例分配内存
- 调用Singleton()的构造函数,初始化成员字段
- 将sinstance对象指向分配的内存空间(此时sinatance就不为空了)
由于java编译器允许处理器乱序执行,以及JDK1.5之前JMM中cache,寄存器到主内存回写顺序的规定,上面的第二和第三的顺序事无法保证的,所以上面的2、3的具体执行顺序是无法保证的。可能为1、2、3也可能为1、3、2。如果1.3.2的话在3执行完毕,2未执行之前,被切换到另一个线程,这时候sInstance因为已经在原线程内执行过了第三点,所以sInstance非空,所以另外的一个线程会直接取走sInstance,再使用时就会出错,这就是DCL失效问题,而且这种难以跟踪难以重现的错误可能会隐藏很久,所以我们在对instance加了volatile字段,在JDK1.5之后,只要将sInstance的定义改为private static volatile Singleton instance;就可以保证sInstance对象每次从主内存读取。就可以使用DCL来实现单例模式。
第五种静态内部类单例模式
public class Singleton{
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.sInstance;
}
private static class SingletonHolader{
private static final Singleton sInstance = new Singleton();
}
}
第一次加载Singleton类时并不会初始化sInstance,只有第一次调用getInstance方法时,虚拟机加载SingletonHolder并初始化sInstance。这样不仅能保证线程安全,也能保证Singleton类的唯一性。(推荐使用)
第六种:枚举单例
public enum Singleton{
INSTANCE;
public void doSomeThing(){}
}
优点:写法简单,默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。
在上述的集中单例实现中在反序列化的情况下会出现重新创建对象。
通过反序列化将一个单例对象写到磁盘,然后再读回来,从而有效地获得一个实例。即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例。相当于调用了该类的构造函数。反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的、被实例化的方法readResolve(),这个方法可以让开发人员控制对象的反序列化。例如,上述几个示例中如果要杜绝单例对象在被反序列化时重新生成对象,那么必须加入如下方法:
private Object readResolve() throws ObjectStreamException{
return sInstance;
}
也就是在readResolve方法中将sInstance对象返回,而不是重新生成一个新的对象。而对于枚举是不存在这个问题的,因为即使反序列化也不会重新生成新的实例。
第七种:通过容器实现单例模式
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.containKey(key)){
objMap.put(key,instance);
}
}
public static Object getService(String key){
return objMap.get(key);
}
}
在程序的开始将多种单例模式类型注入到一个统一的管理类种,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以使用统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了实现,降低了耦合度。