大话设计模式(四)单例模式的优与劣
前言
首先来明白一个问题。那就是在某些情况下,有些对象,我们仅仅须要一个就能够了。比方,一台计算机上能够连好几个打印机,可是这个计算机上的打印程序仅仅能有一个。这里就能够通过单例模式来避免两个打印作业同一时候输出到打印机中。即在整个的打印过程中我仅仅有一个打印程序的实例。
简单说来,单例模式(也叫单件模式)的作用就是保证在整个应用程序的生命周期中,不论什么一个时刻,单例类的实例都仅仅存在一个(当然也能够不存在)。
下图是单例模式的结构图。
以下就来看一种情况(这里先假设我的应用程序是多线程应用程序),演示样例代码例如以下:
public static Singleton GetInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; }
假设在一開始调用 GetInstance()时,是由两个线程同一时候调用的(这种情况是非经常见的),注意是同一时候,(或者是一个线程进入 if 推断语句后但还没有实例化 Singleton 时。第二个线程到达。此时 singleton 还是为 null)这种话,两个线程均会进入 GetInstance()。而后因为是第一次调用 GetInstance(),所以存储在 Singleton 中的静态变量 singleton 为 null ,这种话,就会让两个线程均通过 if 语句的条件推断。然后调用 new Singleton()了。这种话,问题就出来了,因为有两个线程,所以会创建两个实例。
非常显然,这便违法了单例模式的初衷了,
那么怎样解决上面出现的这个问题(即多线程下使用单例模式时有可能会创建多个实例这一现象)呢?
事实上,这个是非常好解决的,能够这样思考这个问题:因为上面出现的问题中涉及到多个线程同一时候訪问这个 GetInstance(),那么能够先将一个线程锁定,然后等这个线程完毕以后,再让其它的线程訪问 GetInstance()中的 if 段语句。演示样例代码例如以下:
public static Singleton GetInstance() { lock(syncRoot){ if (singleton == null) { singleton = new Singleton(); } } return singleton; }
可是假设这种话。每次调用GetInstance方法时都须要lock操作。影响性能。
以下就来又一次改进前面 Demo 中的 Singleton 类,使其在多线程的环境下也能够实现单例模式的功能。
public class Singleton { //定义一个私有的静态全局变量来保存该类的唯一实例 private static Singleton singleton; //定义一个静态对象,且这个对象是在程序运行时创建的。 private static object syncObject = new object(); //构造函数必须是私有的,这样在外部便无法使用 new 来创建该类的实例 private Singleton(){} //定义一个全局訪问点。设置为静态方法,则在类的外部便无需实例化就能够调用该方法 public static Singleton GetInstance() { //这里能够保证仅仅实例化一次,即在第一次调用时实例化。以后调用便不会再实例化 //第一重 singleton == null if (singleton == null) { lock (syncObject) { //第二重 singleton == null if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
上面的就是改进后的代码,能够看到在类中有定义了一个静态的仅仅读对象syncObject,这里须要说明的是,为何还要创建一个 syncObject 静态仅仅读对象呢?
因为提供给 lock keyword的參数必须为基于引用类型的对象。该对象用来定义锁的范围,所以这个引用类型的对象总不能为 null 吧,而一開始的时候。singleton 为 null 。所以是无法实现加锁的。所以必须要再创建一个对象即 syncObject 来定义加锁的范围。
还有要解释一下的就是在 GetInstance()中,我为什么要在 if 语句中使用两次推断 singleton == null ,这里涉及到一个名词 Double-Check Locking ,也就是双重检查锁定。为何要使用双重检查锁定呢?
考虑这样一种情况,就是有两个线程同一时候到达,即同一时候调用 GetInstance(),此时因为 singleton == null ,所以非常明显,两个线程都能够通过第一重的 singleton == null 。进入第一重 if 语句后。因为存在锁机制,所以会有一个线程进入 lock 语句并进入第二重 singleton == null ,而另外的一个线程则会在 lock 语句的外面等待。
而当第一个线程运行完 new Singleton()语句后,便会退出锁定区域。此时,第二个线程便能够进入 lock 语句块。此时,假设没有第二重 singleton == null 的话,那么第二个线程还是能够调用 new Singleton()语句,这样第二个线程也会创建一个 Singleton 实例,这样也还是违背了单例模式的初衷的,所以这里必须要使用双重检查锁定。
细心的朋友一定会发现,假设我去掉第一重 singleton == null ,程序还是能够在多线程下完善的运行的,考虑在没有第一重 singleton == null 的情况下。当有两个线程同一时候到达,此时,因为 lock 机制的存在,第一个线程会进入 lock 语句块。而且能够顺利运行 new Singleton()。当第一个线程退出 lock 语句块时, singleton 这个静态变量已不为 null 了。所以当第二个线程进入 lock 时。还是会被第二重 singleton == null 挡在外面,而无法运行 new Singleton(),所以在没有第一重 singleton == null 的情况下。也是能够实现单例模式的?那么为什么须要第一重 singleton == null 呢?
这里就涉及一个性能问题了。因为对于单例模式的话,new Singleton()仅仅须要运行一次就 OK 了。而假设没有第一重 singleton == null 的话。每一次有线程进入 GetInstance()时,均会运行锁定操作来实现线程同步,这是非常耗费性能的,而假设我加上第一重 singleton == null 的话,那么就仅仅有在第一次,也就是 singleton ==null 成立时的情况下运行一次锁定以实现线程同步,而以后的话,便仅仅要直接返回 Singleton 实例就 OK 了而根本无需再进入 lock 语句块了,这样就能够解决由线程同步带来的性能问题了。
好,关于多线程下单例模式的实现的介绍就到这里了,可是,关于单例模式的介绍还没完。
单例的三种实现方式
以下将要介绍的是懒汉式单例和饿汉式单例
懒汉式单例
何为懒汉式单例呢,能够这样理解。单例模式呢,其在整个应用程序的生命周期中仅仅存在一个实例,懒汉式呢,就是这个单例类的这个唯一实例是在第一次使用 GetInstance()时实例化的,假设不调用 GetInstance()的话,这个实例是不会存在的,即为 null。
形象点说呢,就是你不去动它的话。它自己是不会实例化的,所以能够称之为懒汉。
事实上呢,我前面在介绍单例模式的这几个 Demo 中都是使用的懒汉式单例,看以下的 GetInstance()方法就明白了:
private static volatile TestSingleton instance = null; public static Singleton GetInstance() { if (singleton == null) { lock (syncObject) // synchronized (TestSingleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }
从上面的这个 GetInstance()中能够看出这个单例类的唯一实例是在第一次调用 GetInstance()时实例化的,所以此为懒汉式单例。
另外。能够看到里面加了volatilekeyword来声明单例对象,既然synchronized已经起到了多线程下原子性、有序性、可见性的作用。为什么还要加volatile呢?见參考文献。
双重检測锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型同意所谓的“无序写入”,这也是失败的一个主要原因。因此,为了杜绝“无序写入”的出现,使用voaltilekeyword。
饿汉式单例
上面介绍了懒汉式单例,到这里来理解饿汉式单例的话,就easy多了。懒汉式单例是不会主动实例化单例类的唯一实例的,而饿汉式的话。则刚好相反,他会以静态初始化的方式在自己被载入时就将自己实例化。
以下就来看一看饿汉式单例类。
//饿汉式单例类.在类初始化时,已经自行实例化 public class Singleton1 { //私有的默认构造器 private Singleton1() {} //已经自行实例化 private static final Singleton1 single = new Singleton1(); //静态工厂方法 public static Singleton1 getInstance() { return single; } }
上面的饿汉式单例类中能够看到。当整个类被载入的时候,就会自行初始化 singleton 这个静态仅仅读变量。而非在第一次调用 GetInstance()时再来实例化单例类的唯一实例,所以这就是一种饿汉式的单例类。
登记式单例类(可忽略)
import java.util.HashMap; import java.util.Map; //登记式单例类. //相似Spring里面的方法。将类名注冊。下次从里面直接获取。public class Singleton3 { private static Map<String,Singleton3> map = new HashMap<String,Singleton3>(); static{ Singleton3 single = new Singleton3(); map.put(single.getClass().getName(), single); } //保护的默认构造器 protected Singleton3(){} //静态工厂方法,返还此类惟一的实例 public static Singleton3 getInstance(String name) { if(name == null) { name = Singleton3.class.getName(); System.out.println("name == null"+"--->name="+name); } if(map.get(name) == null) { try { map.put(name, (Singleton3) Class.forName(name).newInstance()); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } return map.get(name); } //一个示意性的商业方法 public String about() { return "Hello, I am RegSingleton."; } public static void main(String[] args) { Singleton3 single3 = Singleton3.getInstance(null); System.out.println(single3.about()); } }
登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记薄)中。对于已经登记过的实例,则从Map直接返回。对于没有登记的。则先登记。然后返回。
这里我对登记式单例标记了可忽略。我的理解来说,首先它用的比較少,另外事实上内部实现还是用的饿汉式单例,因为当中的static方法块,它的单例在类被装载的时候就被实例化了。
好,到这里,就真正的把单例模式介绍完了。在此呢再总结一下单例类须要注意的几点:
一、单例模式是用来实如今整个程序中仅仅有一个实例的。
二、单例类的构造函数必须为私有,同一时候单例类必须提供一个全局訪问点。
三、单例模式在多线程下的同步问题和性能问题的解决。
四、懒汉式和饿汉式单例类。
饿汉式与懒汉式的差别
从速度和反应时间角度来讲,非延迟载入(又称饿汉式)好;从资源利用效率上说。延迟载入(又称懒汉式)好。
饿汉式天生就是线程安全的,能够直接用于多线程而不会出现故障。懒汉式本身是非线程安全的。为了实现线程安全需附加语句。
饿汉式在类创建的同一时候就实例化一个静态对象出来。无论之后会不会使用这个单例,都会占领一定的内存,可是对应的,在第一次调用时速度也会更快,因为其资源已经初始化完毕。
而懒汉式顾名思义,会延迟载入。在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,假设要做的工作比較多,性能上会有些延迟,之后就和饿汉式一样了。
单例对象作配置信息管理时可能会带来的几个同步问题
1.在多线程环境下,单例对象的同步问题主要体如今两个方面,单例对象的初始化和单例对象的属性更新。
本文描写叙述的方法有例如以下假设:
a. 单例对象的属性(或成员变量)的获取,是通过单例对象的初始化实现的。
也就是说,在单例对象初始化时。会从文件或数据库中读取最新的配置信息。
b. 其它对象不能直接改变单例对象的属性。单例对象属性的变化来源于配置文件或配置数据库数据的变化。
1.1单例对象的初始化
首先,讨论一下单例对象的初始化同步。单例模式的通常处理方式是。在对象中有一个静态成员变量,其类型就是单例类型本身;假设该变量为null,则创建该单例类型的对象,并将该变量指向这个对象;假设该变量不为null。则直接使用该变量。
这种处理方式在单线程的模式下能够非常好的运行;可是在多线程模式下,可能产生问题。
假设第一个线程发现成员变量为null,准备创建对象。这是第二个线程同一时候也发现成员变量为null,也会创建新对象。这就会造成在一个JVM中有多个单例类型的实例。
假设这个单例类型的成员变量在运行过程中变化。会造成多个单例类型实例的不一致,产生一些非常奇怪的现象。
比如。某服务进程通过检查单例对象的某个属性来停止多个线程服务,假设存在多个单例对象的实例,就会造成部分线程服务停止,部分线程服务不能停止的情况(此时可考虑使用双重锁安全机制)。
1.2单例对象的属性更新
通常,为了实现配置信息的实时更新,会有一个线程不停检測配置文件或配置数据库的内容,一旦发现变化,就更新到单例对象的属性中。在更新这些信息的时候,非常可能还会有其它线程正在读取这些信息,造成意想不到的后果。
还是以通过单例对象属性停止线程服务为例。假设更新属性时读写不同步。可能訪问该属性时这个属性正好为空(null),程序就会抛出异常。
以下是解决方法。
//单例对象的初始化同步 public class GlobalConfig { private static GlobalConfig instance = null; private Vector properties = null; private GlobalConfig() { //Load configuration information from DB or file //Set values for properties } private static synchronized void syncInit() { if (instance == null) { instance = new GlobalConfig(); } } public static GlobalConfig getInstance() { if (instance == null) { syncInit(); } return instance; } public Vector getProperties() { return properties; } }
这种处理方式尽管引入了同步代码。可是因为这段同步代码仅仅会在最開始的时候运行一次或多次,所以对整个系统的性能不会有影响。
參照读者/写者的处理方式,设置一个读计数器,每次读取配置信息前。将计数器加1,读完后将计数器减1.仅仅有在读计数器为0时,才干更新数据,同一时候要堵塞全部读属性的调用。
代码例如以下:
public class GlobalConfig { private static GlobalConfig instance; private Vector properties = null; private boolean isUpdating = false; private int readCount = 0; private GlobalConfig() { //Load configuration information from DB or file //Set values for properties } private static synchronized void syncInit() { if (instance == null) { instance = new GlobalConfig(); } } public static GlobalConfig getInstance() { if (instance==null) { syncInit(); } return instance; } public synchronized void update(String p_data) { syncUpdateIn(); //Update properties } private synchronized void syncUpdateIn() { while (readCount > 0) { try { wait(); } catch (Exception e) { } } } private synchronized void syncReadIn() { readCount++; } private synchronized void syncReadOut() { readCount--; notifyAll(); } public Vector getProperties() { syncReadIn(); //Process data syncReadOut(); return properties; } }
採用"影子实例"的办法。详细说。就是在更新属性时,直接生成还有一个单例对象实例。这个新生成的单例对象实例将从数据库或文件里读取最新的配置信息;然后将这些配置信息直接赋值给旧单例对象的属性。
public class GlobalConfig { private static GlobalConfig instance = null; private Vector properties = null; private GlobalConfig() { //Load configuration information from DB or file //Set values for properties } private static synchronized void syncInit() { if (instance = null) { instance = new GlobalConfig(); } } public static GlobalConfig getInstance() { if (instance = null) { syncInit(); } return instance; } public Vector getProperties() { return properties; } public void updateProperties() { //Load updated configuration information by new a GlobalConfig object GlobalConfig shadow = new GlobalConfig(); properties = shadow.getProperties(); } }
注意:在更新方法中,通过生成新的GlobalConfig的实例,从文件或数据库中得到最新配置信息,并存放到properties属性中。上面两个方法比較起来,第二个方法更好,首先,编程更简单;其次。没有那么多的同步操作,对性能的影响也不大。
全局变量和单例模式的差别
首先,全局变量就是对一个对象的静态引用。全局变量确实能够提供单例模式实现的全局訪问这个功能。可是。它并不能保证应用程序中仅仅有一个实例。
同一时候。在编码规范中,也明白指出。应该要少用全局变量,因为过多的使用全局变量,会造成代码难读。
还有就是全局变量并不能实现继承(尽管单例模式在继承上也不能非常好的处理,可是还是能够实现继承的)而单例模式的话,其在类中保存了它的唯一实例。这个类,它能够保证仅仅能创建一个实例,同一时候,它还提供了一个訪问该唯一实例的全局訪问点。
单例模式的优与劣
上面哔哔了这么多,言归正传。回到“单例模式的利与弊”问题上来。
总结例如以下:
主要长处
1、提供了对唯一实例的受控訪问。
2、因为在系统内存中仅仅存在一个对象,因此能够节约系统资源,对于一些须要频繁创建和销毁的对象。单例模式无疑能够提高系统的性能。
3、同意可变数目的实例。
主要缺点
1、因为单利模式中没有抽象层。因此单例类的扩展有非常大的困难。
2、单例类的职责过重,在一定程度上违背了“单一职责原则”。
3、滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;假设实例化的对象长时间不被利用。系统会觉得是垃圾而被回收,这将导致对象状态的丢失。
公司面试中。“观察者模式”也会被经常问到及写出代码,下篇博文将会分析解说。
參考资料
1.http://www.iteye.com/topic/652440
2.http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
美文美图