使用最广的设计模式——单例模式

单例模式是我们应用最广的设计模式,所以对于像我这样的菜鸟一样会接触到很多。在使用这种设计模式的时候,单例对象的类必须保证只有一个实例存在。

单例模式的定义

确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

单例模式的适用场景

确保一个类只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。例如创建一个对象要使用的资源过多,或者需要访问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()这句代码不是一句原子指令,大概做了三件事:

  1. 给Singleton的实例分配内存
  2. 调用Singleton()的构造函数,初始化成员字段
  3. 将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获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以使用统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了实现,降低了耦合度。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值