上一篇:《设计模式04—工厂模式》
用来创建独一无二的,只能有一个实例的对象的
应用场景
线程池,缓存,对话框,注册表的对象,日志对象,充当打印机,显卡等
为何使用单例模式
我们利用静态变量,静态方法和适当的访问修饰符也可以做到这一点,但是如果我们将对象赋给一个静态变量,那么我们必须在程序一开始就要创建好这个对象,如果这个对象十分浪费内存,而且程序在执行过程中又没有用到它,就会形成浪费,而单例模式就可以很轻松的实现在需要的时候创建。
剖析经典的单件模式实现
public class Singleton {
//利用一个静态变量来记录Singleton类的唯一实例
private static Singleton uniquerSingleton;
private static Singleton getInstance() {//将构造器声明为私有,只有来自Singleton类内才可以调用构造器
if (uniquerSingleton == null) {
/*如果对象不存在,就利用私有构造器产生 一个实例,
并把它赋值到uniquerSingleton静态变量中,
如果我们不需要,它就永远不会产生,这就是延迟实例化*/
uniquerSingleton = new Singleton();
}
return uniquerSingleton;
}
}
由于Singleton没有公开的构造器(原因是将构造方法设置为私有)这就保证了要想获取Singleton对象,只能通过getInstance方法,所以Singleton对象永远只可能有一个
-
说明: 先不创建实例,当第一次被调用时,再创建实例,所以被称为懒汉式。
-
优点: 延迟了实例化,如果不需要使用该类,就不会被实例化,节约了系统资源。
-
缺点: 线程不安全,多线程环境下,如果多个线程同时进入了 if (uniqueInstance == null) ,若此时还未实例化,也就是uniqueInstance == null,那么就会有多个线程执行 uniqueInstance = new Singleton(); ,就会实例化多个实例;
与之相反的是饿汉式(线程安全)
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
private Singleton() {
}
public static Singleton getUniqueInstance() {
return uniqueInstance;
}
}
-
说明: 实现和 线程不安全的懒汉式 几乎一样,唯一不同的点是,在get方法上 加了一把 锁。如此一来,多个线程访问,每次只有拿到锁的的线程能够进入该方法,避免了多线程不安全问题的出现。
-
优点: 延迟实例化,节约了资源,并且是线程安全的
-
缺点: 虽然解决了线程安全问题,但是性能降低了。因为,即使实例已经实例化了,既后续不会再出现线程安全问题了,但是锁还在,每次还是只能拿到锁的线程进入该方***使线程阻塞,等待时间过长。
后面会重新讲解
下面我们以巧克力工厂为例子来研究单例模式
巧克力工厂
现代化的巧克力工厂具备计算机控制的巧克力锅炉,锅炉做的事就是把巧克力和牛奶融在一起,然后送到下一个阶段以制造成巧克力棒。
这里有一个Choc-O-Holic公司的工业强度巧克力锅炉控制器。看看它的代码,你会发现代码写得相当小心,他们在努力防止不好的事情发生。
例如:排出500加仑的未煮沸的混合物,或者锅炉已经满了还继续放原料,或者锅炉内还没放原料就开始空烧。
/**
* 巧克力锅炉
*/
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
public ChocolateBoiler() {
//初始时锅炉是空的,而且也不在沸腾状态
this.empty = true;
this.boiled = false;
}
public boolean isEmpty() {
return empty;
}
public boolean isBoiled() {
return boiled;
}
/**
* 填充混合物
*/
public void fill() {
/*在锅炉内填入原料时,锅炉必须是空的,
一旦填入原料就必须要把empty和boiled的状态设置好*/
if (isEmpty()) {
empty = false;
boiled = false;
//用于填充牛奶和巧克力
}
}
/**
* 加热混合物
*/
public void boil() {
if (!isEmpty() && !isBoiled()) {
//将状态置为沸腾
boiled = true;
}
}
/**
* 排出混合物
*/
public void drain() {
/*锅炉排出时必须是满的,而且是煮沸的,
排出结束之后需要重新将empty状态设置为空*/
if (!isEmpty() && isBoiled()) {
empty = true;//排出煮沸的巧克力和牛奶
}
}
}
如果同时存在多个ChocolateBoiler实例,可能就会发生很糟糕的事情
下面我们将巧克力锅炉的实例改为单例来避免这些意外发生。
我们还是按照之前的思路,将构造函数设置为私有来隐藏掉,同时设置一个获取实例的函数来控制只产生一个ChocolateBoiler实例。
/**
* 巧克力锅炉——单例模式
*/
public class ChocolateBoilerSingleton {
private boolean empty;
private boolean boiled;
private static ChocolateBoilerSingleton uniqueInstance;
private ChocolateBoilerSingleton() {
//初始时锅炉是空的,而且也不在沸腾状态
this.empty = true;
this.boiled = false;
}
public ChocolateBoilerSingleton getInstance() {
if (uniqueInstance == null) {
System.out.println("创建一个独一无二的巧克力锅炉实例");
uniqueInstance = new ChocolateBoilerSingleton();
}
System.out.println("返回已创建的巧克力锅炉实例");
return uniqueInstance;
}
public boolean isEmpty() {
return empty;
}
public boolean isBoiled() {
return boiled;
}
/**
* 填充混合物
*/
public void fill() {
/*在锅炉内填入原料时,锅炉必须是空的,
一旦填入原料就必须要把empty和boiled的状态设置好*/
if (isEmpty()) {
empty = false;
boiled = false;
//用于填充牛奶和巧克力
}
}
/**
* 加热混合物
*/
public void boil() {
if (!isEmpty() && !isBoiled()) {
//将状态置为沸腾
boiled = true;
}
}
/**
* 排出混合物
*/
public void drain() {
/*锅炉排出时必须是满的,而且是煮沸的,
排出结束之后需要重新将empty状态设置为空*/
if (!isEmpty() && isBoiled()) {
empty = true;//排出煮沸的巧克力和牛奶
}
}
}
接下来我们进行一下测试
public class Test {
public static void main(String args[]) {
ChocolateBoilerSingleton boiler = ChocolateBoilerSingleton.getInstance();
boiler.fill();
boiler.boil();
boiler.drain();
System.out.println("empty:" + boiler.isEmpty() + "\nboil:" + boiler.isBoiled());
// will return the existing instance
ChocolateBoilerSingleton boiler2 = ChocolateBoilerSingleton.getInstance();
}
}
运行结果:
定义单件模式
单件模式确保只有一个类只有一个实例,并提供一个全局访问点。
类图:
单例模式的线程问题
ChocolateBoilerSingleton方法竟然允许在加热的过程中能够使用fill方法,这样就会导致原料的溢出和浪费。
处理多线程
我们只需要把getInstance()变成同步(Synchronized)方法,多线程的问题就可以迎刃而解
public class Singleton {
//利用一个静态变量来记录Singleton类的唯一实例
private static Singleton uniquerSingleton;
/*通过新增synchronized关键字到getInstance方法中,我们迫使每个线程
* 在进入这个方法之前,要先等候别的线程离开这个方法,也就是不允许 两个及两个以上的线程执行此方法*/
private static synchronized Singleton getInstance() {//将构造器声明为私有,只有来自Singleton类内才可以调用构造器
if (uniquerSingleton == null) {
/*如果对象不存在,就利用私有构造器产生 一个实例,
并把它赋值到uniquerSingleton静态变量中,
如果我们不需要,它就永远不会产生,这就是延迟实例化*/
uniquerSingleton = new Singleton();
}
return uniquerSingleton;
}
}
但是这又会遇到另外一个问题就是性能问题,只有第一次执行此方法时,才真正需要同步,换句话说吗,一旦设置了uniquerSingleton变量,那么就不需要进行同步,之后的每一次同步都是累赘。
改善线程性能问题
为了要符合大多数Java应用程序,很明显地,我们需要确保单件模式能在多线程的状况下正常工作。但是似乎同步getInstance()的做法将拖垮性能,该怎么办呢?
1.如果getlnstance()的性能对应困程序不是很关键,就什么都别做
如果你的应用程序可以接受getInstance()造成的额外负担,就忘了这件事吧。同步getInstance()的方法既简单又有效。
但是你必须知道,同步一个方法可能造成程序执行效率下降100倍。因此,如果将getInstance()的程序使用在频繁运行的地方,你可能就得重新考虑了。
2.使“急切”创建实例,而不用延迟突例化的做法
如果应用程序总是创建并使用单件实例,或者在创建和运行时方面的负担不太繁重,你可能想要急切(eagerly〉创建此单件,如下所示:
利用这个做法,我们依赖JVM在加载这个类时马上创建此唯一的单件实例。JVM保证在任何线程访问uniqueInstance静态变量之前,一定先创建此实例。
3.“双重检查加锁”,在getlnstance()中减少使用同步
利用双重检查加锁(double-checked locking),首先检查是否实例已经创建了,如果尚未创建,“才”进行同步。这样一来,只有第一次会同步,这正是我们想要的。
public class SingletonVolatile {
/*利用一个静态变量来记录Singleton类的唯一实例
* volatile关键词确保当uniquerSingleton变量被初始化成SingletonVolatile时,
* 多个线程正确的处理uniquerSingleton变量*/
private static volatile SingletonVolatile uniquerSingleton;
private static SingletonVolatile getInstance() {//将构造器声明为私有,只有来自Singleton类内才可以调用构造器
if (uniquerSingleton == null) {
/*如果对象不存在,就利用私有构造器产生 一个实例,
并把它赋值到uniquerSingleton静态变量中,
如果我们不需要,它就永远不会产生,这就是延迟实例化*/
uniquerSingleton = new SingletonVolatile();
}
return uniquerSingleton;
}
}
这种做法就可以大大减少getInstance()的时间耗费
-
把单件类当成超类,设计子类,但是我遇到了问题:究竟可以不可以继承单件类?
继承单件类会遇到的一个问题,就是构造器是私有的。你不能用私有构造器来扩展类。所以你必须把单件的构造器改成公开的或受保护的。但是这么一来就不算是“真正的”单件了、因为别的类也可以实例化它。如果你真把构造器的访问权限改了,还有另一个问题会出现。
单件的实现是利用静态变量,直接继承会导致所有的派生类共享同一个实例变量,这可能不是你想要的。
所以,想要让子类能工作顺利。基类必须实现注册表(Registry)功能。
在这么做之前,你得想想,继承单件能带来什么好处。就和大多数的模式一样,单件不一定适合设计进入一个库中。 -
为何优先使用单件模式而不是去定义全局变量
在Java中,全局变量基本上就是对对象的静态引用。
在这样的情况下使用全局变量会有一些缺点,我们已经提到了其中的一个:急切实例化VS延迟实例化。
但是我们要记住这个模式的目的:确保类只有一个实例并提供全局访问。
全局变量可以提供全局访问,但是不能确保只有一个实例。全局变量也会变相鼓励开发人员,用许多全局变量指向许多小对象来造成命名空间(namespace)的污染。单件不鼓励这样的现象,但单件仍然可能被滥用。
目前已总结的OO原则:
- 封装变化
- 多用组合,少用继承
- 针对接口编程,不针对实现编程
- 为交互对象之间的松耦合设计而努力
- 类应该对外扩展开放,对修改关闭
- 依赖抽象,不要依赖具体
要点:
- 单件模式确保程序中的一个类只会有一个实例
- 单件模式也提供访问这个实例的全局点。
- 在Java中实现单件模式需要私有的构造器、一个静态方法和一个静态变量。
- 确定在性能和资源上的限制,然后小心地选择适当的方案来实现单件,以解决多线程的问题(我们必须认定所有的程序都是多线程的)
- 如果不是采用第五版的Java 2,双重检查加锁实现会失效。
- 小心,你如果使用多个类加载器,可能导致单件失效而产生多个实例。
- 如果使用JVM 1.2或之前的版本,你必须建立单件注册表,以免垃圾收集器将单件回收。
下一篇:《设计模式06—命令模式》