首先来明白一个问题。那就是在某些状况下,有些对象,咱们仅仅需要一个就可以了。比方,一台计算机上可以连好几个打印机,但是这个计算机上的打印程序仅仅能有一个。这里就可以经过单例模式来避免两个打印做业同一时候输出到打印机中。即在整个的打印过程当中我仅仅有一个打印程序的实例。html
简单说来,单例模式(也叫单件模式)的做用就是保证在整个应用程序的生命周期中,不论什么一个时刻,单例类的实例都仅仅存在一个(固然也可以不存在)。java
下图是单例模式的结构图。面试
如下就来看一种状况(这里先假设个人应用程序是多线程应用程序),演示样例代码例如如下:数据库
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 类,使其在多线程的环境下也可以实现单例模式的功能。post
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、单例模式是用来实现在整个程序中仅仅有一个实例的。
2、单例类的构造函数必须为私有,同一时候单例类必须提供一个全局訪问点。
3、单例模式在多线程下的同步问题和性能问题的解决。
4、懒汉式和饿汉式单例类。
从速度和反应时间角度来说,非延迟载入(又称饿汉式)好;从资源利用效率上说。延迟载入(又称懒汉式)好。
饿汉式天生就是线程安全的,可以直接用于多线程而不会出现故障。懒汉式自己是非线程安全的。为了实现线程安全需附加语句。
饿汉式在类建立的同一时候就实例化一个静态对象出来。不管以后会不会使用这个单例,都会占领必定的内存,但是对应的,在第一次调用时速度也会更快,由于其资源已经初始化完毕。
而懒汉式顾名思义,会延迟载入。在第一次使用该单例的时候才会实例化对象出来,第一次调用时要作初始化,假设要作的工做比較多,性能上会有些延迟,以后就和饿汉式同样了。
1.在多线程环境下,单例对象的同步问题主要体现在两个方面,单例对象的初始化和单例对象的属性更新。
本文描写叙述的方法有例如如下假设:
a. 单例对象的属性(或成员变量)的获取,是经过单例对象的初始化实现的。
也就是说,在单例对象初始化时。会从文件或数据库中读取最新的配置信息。
b. 其它对象不能直接改变单例对象的属性。单例对象属性的变化来源于配置文件或配置数据库数据的变化。
首先,讨论一下单例对象的初始化同步。单例模式的一般处理方式是。在对象中有一个静态成员变量,其类型就是单例类型自己;假设该变量为null,则建立该单例类型的对象,并将该变量指向这个对象;假设该变量不为null。则直接使用该变量。
这种处理方式在单线程的模式下可以很是好的运行;但是在多线程模式下,可能产生问题。
假设第一个线程发现成员变量为null,准备建立对象。这是第二个线程同一时候也发现成员变量为null,也会建立新对象。这就会形成在一个JVM中有多个单例类型的实例。
假设这个单例类型的成员变量在运行过程当中变化。会形成多个单例类型实例的不一致,产生一些很是奇怪的现象。
好比。某服务进程经过检查单例对象的某个属性来中止多个线程服务,假设存在多个单例对象的实例,就会形成部分线程服务中止,部分线程服务不能中止的状况(此时可考虑使用双重锁安全机制)。
一般,为了实现配置信息的实时更新,会有一个线程不停检測配置文件或配置数据库的内容,一旦发现变化,就更新到单例对象的属性中。在更新这些信息的时候,很是可能还会有其它线程正在读取这些信息,形成意想不到的后果。
仍是以经过单例对象属性中止线程服务为例。假设更新属性时读写不一样步。可能訪问该属性时这个属性正好为空(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