设计模式之单例模式

之前在看设计模式之禅这本书,本来想用一个专题专门讲设计模式,可是越看越觉得设计模式其实很难,非我等渣渣之辈三言两语就能解释清楚的,所以心想就一点点来,先在简单介绍自己所知道的单例模式,总结归纳方便记忆。

一,单例模式的定义与应用

单例模式是一种比较简单的模式:在这种模式里,确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为,对于代码开发中,一个类同时只有一个实例对象的情况叫做单例。如果我们想保证一个类只有一个对象,就需要在它的构造方法上私有化,然后提供一个getInstance方法判断该类实例是否存在,存在即返回,不存在则创建一个再返回。

package designPattern;

//非线程安全懒汉单例
public class Singleton {
	//静态创建单例对象
	private static Singleton instance;
	//构造函数私有化,这样该类就不会实例化
	private Singleton() {}
	//获取唯一可用的对象
	public static Singleton getInstance() {
		if(instance == null)
			instance = new Singleton();
		return instance;
	}

}

这种方式是最简单的单例模式,不过上边的代码并没有考虑线程安全的场景。

同时这种实现单例的方式被称为懒汉模式,所谓懒汉,指的是只有在需要对象的时候才会调用方法生成单例。

二,单例与线程安全

在上边的代码中,如果是多线程场景,两个线程同时执行到“if(instance == null)”这行代码都会判断通过,然后各自生成一个instance。所以要实现线程安全的懒汉模式单例,需要在getInstance方法上增加synchronized。通过锁解决并发问题:

package designPattern;

//线程安全懒汉单例
public class Singleton {
	//静态创建单例对象
	private static Singleton instance;
	//构造函数私有化,这样该类就不会实例化
	private Singleton() {}
	//获取唯一可用的对象
	public static synchronized Singleton getInstance() {
		if(instance == null)
			instance = new Singleton();
		return instance;
	}

}

不过,这种写法的问题是效率很低,因为只有第一次初始化的时候我们才需要并发控制,大多数情况下是不需要synchronized同步的。

双重检验锁

由于synchronized可以定义同步代码块,这样同步粒度就要比同步方法少一点,从而效率高一点。

package designPattern;

//双重检验锁单例模式
public class Singleton {
	private volatile static Singleton singleton;
	private Singleton() {};
	public static Singleton getSingleton() {
		if(singleton == null) {    //B
			synchronized(Singleton.class) {
				if(singleton == null) {
					singleton = new Singleton();    //A
				}
			}
		}
		return singleton;    //B
	}

}

双重检验的原因就是防止多个进程同时执行代码段,造成生成多个实例;而另一方面,第一次检验就可判断是否存在实例,不需要全部访问都进行同步限制。

还有值得注意的是,这种实现方法静态变量singleton必须通过volatile来修饰以防止初始化的指令重排,否则可能被引用到一个未初始化完成的对象。关于volatile的解释,下面这段话摘自《深入理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2)它会强制将对缓存的修改操作立即写入主存;

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

所以在上边的代码中,如果没有防止指令重排,线程B读到第一个判断if(singleton == null)的时候,线程A正好在生成实例不过未完成,B就会返回一个未完成初始化的实例。

饿汉模式

package designPattern;

//饿汉模式
public class Singleton {
	private static Singleton instance = new Singleton();
	private Singleton() {};
	public static Singleton getInstance() {
		return instance;
	}
}

饿汉模式中静态变量是随着类加载时完成初始化的,保证instance可以在类初始化的时候被实例化。

由于类初始化是由classloader实现的,在加载类的过程中使用synchronized关键字,所以这个方法在整个装载过程中是线程安全的。缺点:类加载时就初始化,浪费内存。

单例模式静态内部类实现

package designPattern;

//静态内部类实现
public class Singleton {
	private static class SingletonHolder{
		private static final Singleton INSTANCE = new Singleton();
	}
	private Singleton() {}
	public static final Singleton getInstance() {
		return SingletonHolder.INSTANCE;
	}
}

在这种方法中Singleton类被装载,instance不一定被初始化因为Singleton类没有被主动使用,只有显示通过调用getInstance方法时才会显示装载SingletonHolder类,从而实例化。

所以这种方法即像饿汉模式那样通过classloader实现线程安全,又兼顾懒汉模式的lazy-loading功能。

三,单例与序列化

单例模式把构造方法设置为私有方法来避免外部调用,不过我们可以利用反射调用类中的私有构造方法破坏单例,而一种容易被忽视的场景就是对象的序列化和反序列化。

比如在进行反序列化时,有些方法会通过反射的方式调用无参构造方法新建一个对象。所以单例模式对象进行序列化和反序列化时要考虑到单例被破坏的情况。

package designPattern;

import java.io.Serializable;

//重写readResolve方法
public class Singleton implements Serializable{
	private static class SingletonHolder{
		private static final Singleton INSTANCE = new Singleton();
	}
	private Singleton() {}
	public static final Singleton getInstance() {
		return SingletonHolder.INSTANCE;
	}
	
	private Object readResolve() {
		return SingletonHolder.INSTANCE;
	}
	
}

 

四,单例的最佳实现方式

Effective Java中写道:使用枚举实现单例的方法虽然没有被广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

package designPattern;

//枚举实现单例
public enum Singleton{
	INSTANCE;
	public void whateverMethod() {
		
	}
}

好处是:1. 多线程安全

2. lazy-loading

3. 防止反序列化重新创建新的对象,绝对防止多次实例化

枚举enum的序列化和反序列化是特殊定制的,可以避免3出现的问题,不过用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值