独一无二的对象
单例模式的类图可以说是所有设计模式中最简单的,事实上,它的类图上只有一个类!但是可不要兴奋过头,尽管从类设计的视角来说它很简单,但是实际上还是会遇到相当多的波折。
有一些对象其实我们只需要一个,比方说:线程池、缓存、对话框、注册表……事实上,这类对象只能有一个实例,如果制造出多个就会导致许多问题产生。
许多时候,的确通过程序员之间的约定就可以办到。但如果有更好的做法,大家应该都乐意接受。单例模式是经得起时间考验的方法,可以确保只有一个实例会被创建。单例模式也给了我们一个全局的访问点,和全局变量一样方便,又没有全局变量的缺点。
举例来说,如果将对象赋值给一个全局变量,那么你必须在程序一开始就创建好对象,万一这个对象非常耗费资源,而程序在这次的执行过程中又一直没用到它,就形成了浪费。
经典的单例模式
我们正在把某个类设计成自己管理的一个单独实例,同时也避免其他类再自行产生实例。想要取得单例实例,通过单例类是唯一的途径。
我们也提供对这个实例的全局访问店,当你需要实例时,想类查询,它会返回单个实例。
处理多线程
在多线程下,前面的代码可能不会运行的很好。比如:若线程A在执行if(uniqueInstance==null)后被挂起(已经进入if的语句块),切换至线程B,那么这时uniqueInstance对象还是null。接下来线程B在调用getInstance()方法创建实例后被挂起,线程A被激活,继续执行uniqueInstance = new Singleton()这段代码,那么此时程序中就会有两个Singleton的实例。
只要把getInstance()变成同步(sunchronized)方法,多线程灾难几乎就可以轻易地解决了:
通过增加sunchronized关键字到getInstance()方法中,我们迫使每个线程在进入这个方法之前,要先等候到别的线程离开该方法。也就是说,不会有两个线程可以同时进入这个方法。
但是同步会降低性能,其实,只有第一次执行此方法时,才真正需要同步。换句话说,一旦设置好uniqueInstance变量,就不再需要同步这个方法了。之后每次调用这个方法,同步都是一种累赘。
能改善多线程吗?
1.如果getInstance()的性能对应用程序不是很关键,就什么都别做。
如果你的应用程序可以接受getInstance()造成的额外负担,就忘了这件事吧。同步getInstance()方法既简单又有效。但是你必须知道,同步一个方法可能造成程序执行效率下降100倍。因此,如果将getInstance()的程序使用在频繁运行的地方,你可能就得重新考虑了。
2.使用“急切”创建实例,而不用延迟实例化的做法。
如果应用程序总是创建并使用单例实例,或者在创建运行时方法的附带不太繁重,你可能想要会想要一下这种实现方式:
public class Singleton { // 需要进行单例的对象。 private static Singleton uniqueInstance = new Singleton(); // 该对象不能使用new进行创建。 private Singleton() {} // 对象可以通过该方法进行创建。 public static synchronized Singleton getInstance() { return uniqueInstance; }}
利用这个做法,我们依赖JVM在加载这个类时马上创建次唯一的单例实例。JVM保证在任何线程访问uniqueInstance静态变量之前,一定先创建次实例。
3.用“双重检查加锁”,在getInstance()中减少使用同步。
利用双重检查加锁(double-checked locking),首先检查是否实例已经创建了,如果尚未创建,“才”进行同步。这样一来,只有第一次会同步,这正是我们想要的。来看看代码:
public class Singleton { // 需要进行单例的对象。 private volatile static Singleton uniqueInstance = null; // 该对象不能使用new进行创建。 private Singleton() {} // 对象可以通过该方法进行创建。 public static Singleton getInstance() { // 如果实例不存在,就进入同步区。 if(uniqueInstance == null) { synchronized (Singleton.class) { if(uniqueInstance == null) { // 进去区块后,再检查一次。如果仍是null,才创建实例。 uniqueInstance = new Singleton(); } } } return uniqueInstance; }}
volatile关键词确保,当uniqueInstance变量被初始化成Singleton实例时,多个线程正确地处理uniqueInstance变量。
如果性能是你关心的重点,那么这个做法可以帮助你大大减少getInstance()的时间耗费。
很不幸地,在1.4及更早版本的java中,许多JVM对于volatile关键字的实现会导致双重锁检查加锁失败。如果你不能使用Java5,必须使用旧版的Java,就请不要利用此技巧实现单例模式。