设计模式目录:
一、什么是单件模式(Singleton Pattern)?
用来创建独一无二的,只能有一个实例的对象的入场券。
确保一个类只有一个实例,并提供一个全局访问点。
单件模式的类图可以说是所有设计模式的类图中最简单的,其类图上只有一个类。尽管从类设计的视角来说它很简单,但是实现上还是会遇到相当多的波折。
单件模式的类图:
下面来详细探究一下。
1.1、有什么用处?
有一些对象其实我们只需要一个,比如:线程池(threadpool)、缓存(cache)、处理偏好设置和注册表(registry)的对象、日志对象等。事实上,这类对象只能有一个实例,如果有多个实例,就会导致许多问题产生。例如,程序异常、资源使用过量,或是不一致的结果。
1.2、有一些类的确应该只有一个实例,为什么不通过程序员之间互相约定或者用全局变量来实现?如,Java的静态变量就可以做到。
许多时候,程序员互相约定是可以做到,但是有更好的办法,应该使用更好的这种。单件模式是经过时间考验的方法,可以确保只有一个实例会被创建。单件模式也给了我们一个全局的访问点,和全局变量一样方便,又没有全局变量的缺点。
1.3、使用全局变量有什么缺点?
例如:如果将对象赋值给一个全局变量,那么你必须在程序一开始就创建好对象(这里也和JVM的实现有关,有些JVM的实现是:在用到的时候才创建对象),假如这个对象非常耗费资源,而程序在这次的执行过程中又一直没有用到,不就浪费了?利用单间模式,有时我们可以控制它去实现:需要时才创建对象。
单件模式听起来简单,但如何保证一个对象只能被实例化一次,并能够解决相应存在的问题,并不是那么简单可以说出来。
二、如何创建一个对象?
2.1、众所周知的:new MyObject();
2.2、经典的单件模式实现(注意:这种代码是有问题的,后文会详细讲到):
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
我们用下图来分析阐释:
2.3、到此,我们初步的来小结一下:
- 单件模式的对象实例是独一无二的,在任何时刻都只有一个对象,可以确保程序中使用的全局资源只有一份
- 经常被用来管理共享的资源,如数据库连接或者线程池
- 能够确保这个对象是单件的原因是:没有公开的构造器,使用私有构造器创建对象。
- 想要取得单件对象,必须“请求”得到一个实例,而不是自行实例化得到一个实例。类中有一个静态方法getInstance(),调用这个方法,就可以获取。这个对象可能是本次调用才创建出来的,也有可能是以前早就被创建出来了。
三、单件模式中存在的问题
3.1、多线程问题:当有两个线程同时执行上述代码时,此时可能会出现什么问题?
可以将上述代码用两个线程同时访问,会很容易看到,会被创建两个Singleton对象出来。
处理办法:
3.1.1、只要把getInstance()方法编程同步synchronized方法,多线程问题就可以轻易解决了
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
这种方法的确可以轻易解决问题,但是同步会降低性能,从而又是一个性能问题。并且,这个单件对象的获取,只有在第一次获取时才会进行同步来避免多线程问题,如果按照上述同步方法,在接下来后的每次调用时,同步方法就是累赘。
小结:如果getInstance()的性能对于应用程序不是很关键,就直接用这种方法解决问题好了。你的程序能够接受这种性能上的损失,就用这种方式,但如果频繁被调用和使用,那么就要慎重考虑了。
3.1.2、使用“急切”创建实例,而不是用”延迟实例化“的做法
如果应用程序总是创建并使用单件实例,或者在创建和运行时方面的负担不太繁重,你可能想要急切(eagerly)创建此单件:
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return uniqueInstance;
}
}
利用这个做法,就可以在JVM加载这个类时,立刻创建唯一的单件实例,JVM保证在任何线程访问uniqueInstance静态变量之前,一定先创建此实例。
3.1.3、用“双重检查加锁”,在getInstance()中减少使用同步
利用双重检查加锁(double-checked locking),首先检查是否实例已经创建好了,如果尚未创建,“才”进行同步,这样一来,只有第一次会同步,这样就完美切合了要求。如下代码:(此种方式不适合jdk1.4及更早版本)
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
分析:
如果性能是你关心的重点,那么这个做法可以大大地减少getInstanc()的时间耗费。
四、总结(包含一些其他可能的问题)
- 单件模式确保程序中一个类最多只有一个实例
- 单件模式也提供访问这个实例的全局点
- 在Java中实现单件模式需要私有的构造器、一个静态方法和一个静态变量
- 确定在性能和资源上的限制,然后小心地选择适当的方案来实现单件,以解决多线程的问题(我们必须认定所有的程序都是多线程的)
- 如果不是采用第五版的Java 2,双重检查加锁实现会失效
- 小心,你如果使用多个类加载器,可能导致单件失效而产生多个实例
- 如果使用JVM1.2或之前的版本,你必须建立单件注册表,以免垃圾回收器将单件回收
红色第6条的解决办法之一:自行指定类加载器,并指定同一个类加载器
注:学习自《Head First 设计模式》