- 一单例模式介绍
- 什么是单例模式
- 单例模式UML图
- 单例模式应用场景
- 二单例模式的简单示例
- 几种实现方式
- 懒汉1线程不安全
- 懒汉2线程安全
- 双重校检锁线程安全
- 饿汉静态成员变量
- 静态内部类
- 枚举线程安全且防反序列化
- 容器实现单例模式
- 如何防止反序列化重建对象
- 实现方式小结
- 几种实现方式
- 三Android中单例模式范例
- LayoutInflater
- 四总结
一、单例模式介绍
什么是单例模式
单例模式就是在整个全局中(无论是单线程还是多线程),该对象只存在一个实例,而且只应该存在一个实例,没有副本(副本的制作需要花时间和空间资源)。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,同时该对象需要协调系统整体的行为,单例模式是最好的解决方案。单例模式相当于只有一个入口的系统,使得所有想要获取该系统资源的对象都要经过该入口。
不管是在单线程应用还是多线程并发的应用,单例模式的使用都是一样的,只是在多线程并发的情况下,对于单例模式的实现方式需要加同步管理机制。
单例模式UML图
单例模式应用场景
确保某个类有且只有一个对象,避免产生多个对象消耗过多的资源(时间、空间),或者某种类型的对象只应该有一个的特殊情况。同时,使用单例模式能够体现共享和同步的思想,因为单例就是全局的意思,全局即共享,它需要协调系统的整体行为。因此,使用单例模式往往是与同步分不开的。
例如:
- I/O流操作
对于I/O流操作来说,创建过程复杂而且消耗很多资源。同时,因为输入流只是一个渠道,而且它需要保证一致性(即当前操作之后的效果是会影响到以后的使用的),单纯从程序的实现上它并没有多样化的需求。比如说:创建某一个文件的输入流,那么该输入流的任务就是将文件从外存读入内存当中而已,你如果重新创建另一个输入流的话,那么一切又将重新开始,因为是另外的一个输入流对象,它的信息不一致了。如果你要让他保持一致性,那么要额外花费更多的开销,因此倒不如直接就是单例模式。
- 数据库连接
对于数据库来说,一个应用甚至多个应用都可以只对应一个数据库。同时,对于数据库访问来说,需要有一个连接池管理,以及同步管理来限制链接的数量和数据的同步。如果重新创建了一个数据库连接对象,那么当前创建的对象链接信息将与之前的信息不一致,造成程序隐患或者崩溃(如果要使得一致,那么又要重新对该对象进行资源初始化)。同样还是要保证一致性,因此单例在这里正好可以限制程序实现一个连接池,以及同步的机制,节省资源和时间。
上面的两种对象的创建都需要消耗内存和时间,而且由于他们需要保证前后一致性,因此也只应该有一个实例。
二、单例模式的简单示例
实现单例模式主要有如下几个关键点:
(1)构造函数不对外开放,一般为private
(2)通过一个静态方法或者枚举返回单例对象
(3)确保单例类的对象有且只有一个,尤其是在多线程的环境下
(4)确保单例类对象在反序列化时不会重新构建对象
上述关键点中,(1)、(2)两点容易实现,(3)、(4)比较麻烦,许多单例模式的实现都是在单线程下的,多线程下的单例模式就需要加上同步机制,而当需要把对象刻到磁盘上存放时,就会牵扯到反序列化的问题。因此单例模式在(3)、(4)的应用情况下需要特别处理。下面介绍几种实现单例模式的方式,他们有些是线程不安全的,有些是线程安全的;对于反序列化重构对象,只有枚举可以防止。
注意:对于反序列化,系统提供一个方法让程序猿控制对象的反序列化。因此,对于反序列化我们可以自己覆盖该方法进行处理。
几种实现方式
懒汉1(线程不安全)
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
该方法在多线程环境下无法实现单例,无法防止反序列化重新构建对象。
优点:最为简单;之所以称为懒汉,是因为它把单例初始化延迟到第一次调用
getInstance方法上。
缺点:线程不安全
懒汉2(线程安全)
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
使用synchronized关键字修饰getInstance方法,这样可以保证一般情况下单例对象的唯一性,但是会产生另一个问题:即使已经创建了单例,每次调用getInstance方法还是会进行同步(同步资源竞争处理)。这样浪费了不必要的资源,同时也将单例模式节省资源的性能降低。这是懒汉模式(线程安全)的一大弊病。
优点:线程安全
缺点:99%的同步是多与的
双重校检锁(线程安全)
public class Singleton1 {
private static Singleton1 instance = null;
private static Object synObj = new Object();
private Singleton1(){
}
public static Singleton1 getInstance(){
if (instance == null) {//先判断有没有实例化
synchronized(synObj){//如果没有被实例化就请求锁
if (instance == null) {//得到锁之后,再次判断是否已经被先前获得锁的对象实例化
return instance = new Singleton1();
}
}
}
return instance;
}
}
双重校检锁,把懒汉模式的弊病避免了,它首先判断是否已经有实例,然后再去竞争锁,竞争到锁了之后再一次判断,这样就可以避免在对象已经被实例化的情况下参与锁的竞争。
优点:单例唯一,线程安全
缺点:JDK1.6以上;第一次加载比较慢;偶尔会失败(双重检查锁定失效)
饿汉(静态成员变量)
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
使用静态成员变量只创建一次的特性实现单例模式,静态成员变量只创建一次这个特性是由classLoader管理,它可以保证该成员在整个全局只被初始化一次。
优点:代码简洁,不需要同步锁
缺点:之所以称为饿汉,是因为只要编译器一看到该类就会初始化单例,无法达到
延迟加载的目的。
静态内部类
package designmodle.singleton;
/**
* @author David
*
*/
public class Singleton3 {
private static class SingletonHolder{
private static final Singleton3 instance = new Singleton3();
}
private Singleton3(){}
public static Singleton3 getInstance(){
return SingletonHolder.instance;
}
}
与饿汉(静态成员变量)方法同样是使用classLoader机制,比饿汉好的地方在于它能够延迟加载,当第一次加载Singleton类时并不会初始化instance,只有在第一次调用getInstance方法时才会导致SingleHolder被加载,同时instance被初始化。
优点:线程安全,对象唯一性,延迟实例化
缺点:暂无
枚举(线程安全,且防反序列化)
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
一看就知道,如此简单!枚举是最简单的实现单例模式的方法,而且最重要的是枚举默认是线程安全的,同时,还可以防止反序列化!
枚举的这种方式很少有人使用,但是相当之简单!而且完全符合单例模式的全部关键要素。
优点:实例唯一、线程安全、防止反序列化重构、简单,简单,简单!
缺点:JDK1.5以上
容器实现单例模式
如果程序中有许多单例类别,那么可能会需要一个容器类别进行管理,因此我们也可以通过容器进行实现多种类单例模式,同时使用了容器就能够使得获得单例接口统一,降低耦合度。
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.containsKey(key)){
objMap.put(key,instance);
}
}
public static Object getService(String key){
return objMap.get(key);
}
}
这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户成本,也对用户隐藏了获取单例的具体实现(不用知道单例的类名和获取方法),降低了使用者与被使用单例类的耦合度。
优点:单例唯一,线程安全(指的是多线程情况下获得的是同样的单例,HashMap本身并不是线程安全的),单例类别管理,降低单例类别与用户之间的耦合。
缺点:暂无
如何防止反序列化重建对象
如何防止反序列化带来的问题,其实很简单。反序列化可以通过特殊的途径创建一个类的新实例而不管该类的构造函数的可见性。但是系统给我们提供了一个很特别的hook方法,是专门让开发人员控制对象的反序列化,该方法就是:readResolve(),我们可以在这个方法内部杜绝单例对象在背反序列化时重新生成对象。
加入方式很简单,举静态内部类的方式为例:
public class Singleton3 implements Serializable {
private static class SingletonHolder{
private static final Singleton3 instance = new Singleton3();
}
private Singleton3(){}
public static Singleton3 getInstance(){
return SingletonHolder.instance;
}
//添加这个hook函数,那么系统在反序列化的过程中就会通过该Hook方法得到原有的单例
//而不是重新创建一个单例。
private Object readResolve() throws ObjectStreamException{
return SingletonHolder.instance;
}
}
实现方式小结
无论哪种形式实现单例模式,它们的核心原理都是将构造函数私有化,并且通过静态方法获取一个唯一的实例,在这个获取的过程中保证线程安全、防止反序列化导致的对象重新生成等问题,选择哪种方式取决于项目本身。
(1)是否是复杂的并发环境
(2)JDK版本是否过低
(3)单例对象的资源消耗等
三、Android中单例模式范例
在Android系统中,我们经常会通过Context获取系统级别的服务,如WindowsManagerService、ActivityManagerService等,更加常用的是一个LayoutInflater的类,这些服务会在何时的时候以单例的形式注册在系统中,在我们需要的时候就通过Context的getSystemService(String name)获取,我们会看到Android中是使用容器的方式来多种服务的单例管理。我们以LayoutInflater为例来说明,平常我们使用LayoutInflater较为常见的地方是在ListView的getView()策略方法中:
LayoutInflater
@Override
public View getView(int position,View convertView, ViewGroup parent){
ViewHolder holder = null;
if(null == convertView){
holder = new ViewHolder();
convertView = LayoutInflater.from(mContext).inflate(mLayoutId,null);
//代码省略
}else{
//代码省略
}
//代码省略
return convertView;
}
我们是通过使用LayoutInflater.from(Context)来获取LayoutInflater服务的,下面看看LayoutInflater.from(Context)的实现:
public static LayoutInflater from(Context context){
//获取系统的LayoutInflater服务
LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if(LayoutInflater == null){
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
我们从上面的代码可以看到,from(Context)方法是调用Context类的getSystemService(String key)方法。Context是一个抽象类别,那在Android中就一定还有它的实现类来操作Context的功能,在Application、Activity、Service中都会有一个Context对象,即Context的总个数为:Activity个数+Service个数+1(Application)。而ListView都是显示在Activity中的,因此我们可以查阅Activity的入口ActivityThread的main函数,对Activity的创建进行跟踪,可以发现最终在handleBindApplication方法中函数中创建了一个ContextImpl对象,而该对象就是Context的实现类。
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
if (mApplication != null) {
return mApplication;
}
Application app = null;
String appClass = mApplicationInfo.className;
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}
try {
java.lang.ClassLoader cl = getClassLoader();
//创建了ContextImpl类
ContextImpl appContext = new ContextImpl();
appContext.init(this, null, mActivityThread);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
} catch (Exception e) {
if (!mActivityThread.mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to instantiate application " + appClass
+ ": " + e.toString(), e);
}
}
...
}
ContextImpl类:
class ContextImpl extends Context{
//代码省略
//ServiceFecher抓取器,通过getService获取各类服务对象
static class ServiceFecher{
//当前服务在容器中所处的位置,便于下面同步块中获取对应的服务
int mContextCacheIndex = -1;
//获取系统服务
public Object getService(ContextImpl ctx){
ArrayList<Object> cache = ctx.mServiceCache;
Object service;
//进行同步加锁控制
synchronized(cache){
if(cache.size()==0){
for(int i=0;i< sNextPerContextServiceCacheIndex;i++){
cache.add(null);
}
}else{
//缓存非空,从缓存中获取Service对象
sercice = cache.get(mContextCacheIndex);
if(service != null){
return service;
}
}
//hook方法,当缓存中的Service为空时,重新创建
service = createService(ctx);
cache.set(mContextCacheIndex,service);
return service;
}
}
/**
*hook方法,让子类重写该方法用以创建服务对象
*/
public Object createService(ContextImpl ctx){
throw new RuntimeException("Not implemented");
}
}
//Service容器,作为各种单例的存放容器
private static final HashMap<String,ServiceFetcher> STSTEM_SERVICE_MAP = new HashMap<String,ServiceFetcher>();
//服务记录数指针,记录存放至容器的下一个服务位置
private static int sNextPerContextServiceCacheIndex = 0;
//注册服务
private static void registerService(String serviceName,ServiceFetcher fetcher){
if(!(fetcher instanceof StaticServiceFetcher)){
//标记服务在容器中的位置
fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++;
}
//将服务添加到Service容器中
SYSTEM_SERVICE_MAP.put(serviceName,fetcher);
}
//静态语句块,第一次加载该类执行
static {
//代码省略
//注册LayoutInflater Service
registerService(LAYOUT_INFLATER_SERVICE,new ServiceFetcher(){
//实现创建服务的hook方法
public Object createService(ContextImpl ctx){
return PolicyManager.makeNewLayoutInflater(
ctx.getOuterContext());
}
});
}
//。。。。。。。。。。。
//代码省略
//通过key获取对应的服务
@Override
public Object getSystemService(String name) {
//根据name来获取服务选择器
ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
return fetcher == null? null: fetcher.getService(this);
}
}
从ContextImpl的部分代码中可以看到,ContextImpl是采用容的方式实现的单例模式。在虚拟机第一次加载该类时会注册各种ServiceFetcher,其中就包含了LayoutInflater Service。将这些服务以键值对的形式存储在一个HashMap中,用户使用时只需要根据key来获取对应的ServiceFetcher,然后通过ServiceFetcher对象的getService函数来获取具体的服务对象,当第一次获取时,会调用它的hook方法createService函数创建服务对象,然后将该对象缓存到一个列表中,下次再取的时候直接从缓存中获取,避免重复创建对象,从而达到单列的效果。这种模式下,系统的核心服务以单例的形式存在,减少了资源的消耗。
四、总结
单例模式是应用最广的模式之一,也是许多人最初接触并使用的设计模式。在我们的系统只需要一个全局的对象时,我们就可以使用它来节省系统资源并达到协调系统整体行为的目的。在应用这个模式的时候,要注意所说的4个关键点:构造函数私有化、一个单例、多线程安全、防止反序列化重建对象。