在所有的设计模式里面,单例模式算是最简单的了。在这里先说说单例模式在哪些情况下使用了?
在实际开发中,有些对象我们只需要一个:线程池、缓存、硬件设备等;如果多个实例会造成有冲突、结果的不一致性等问题,都可以使用单例模式来解决。那么有人会说,不是用单例模式也是可以的,比如说:可以用静态变量方式来实现,或者程序员之间协商一个全局变量,有好多种解决方案。我们为什么要使用单例模式了?单例模式作为前人经验的总结,在实现以及时间复杂度上面,都是比较优秀的解决方案。其他解决办法有可能灵活度不够,而且每个程序员写的代码的质量是不一样的,这样时间复杂度也是一个不要控制的因素。
什么是单例模式?
单例模式就是确保一个类最多只有一个实例,并提供一个全局访问点。
单例模式类图如下:
单例模式代码实现如下:
package com.designpatterns.singletonmode;
public class Singleton {
/*
* 静态的变量会存放这个对象
*/
private static Singleton uniqueInstance = null;
private Singleton(){
};
public static Singleton getInstance(){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
在上面的代码示例中,首先判断uniqueInstance是不是null,如果是第一次调用,uniqueInstance是null,就new一个对象,如果不为空,说明已经调用过了。直接返回其实我们有了这个思路,可以简单的扩展一下,就是说如果我们这个类的对象他有两个或者三个,那我们这个时候也可以使用private构造函数,然后关掉了类外面构造对象的这条路。这样的话就可以在这个类的里面来构造自己,控制外面类有这个对象,有一个就是单例,有两个或者三个,也是一样的意义,要做到触类旁通。
使用单例模式实现一个简单的巧克力工厂的例子,加深一下学习。首先是一般实现,代码示例:
package com.designpatterns.singletonmode;
public class ChocolateFactory {
private boolean empty;
private boolean boiled;
//正常的设计方式
public ChocolateFactory(){
empty = true;
boiled = false;
}
public void fill(){
if(empty){
//添加巧克力动作
empty = false;
boiled = false;
}
}
public void drain(){
if((!empty) && boiled){
//排出巧克力动作
empty = true;
}
}
public void boil(){
if((!empty) && (!boiled)){
//煮沸
boiled = true;
}
}
}
上面实现方式按照正常的Java设计思想做的,在使用的时候,可以实例化出很多的巧克力工厂对象,但是我们知道,巧克力工厂是一个硬件设备,只有一个,在使用的时候,只允许有一个对象,不然会出现不可控制的问题。
使用单例模式重新设计:
package com.designpatterns.singletonmode;
public class SingletonChocolateFactory {
private boolean empty;
private boolean boiled;
public static SingletonChocolateFactory uniqueInstance = null;
private SingletonChocolateFactory(){
empty = true;
boiled = false;
}
public static SingletonChocolateFactory getInstance(){
if(uniqueInstance == null){
uniqueInstance = new SingletonChocolateFactory();
}
return uniqueInstance;
}
public void fill(){
if(empty){
//添加巧克力动作
empty = false;
boiled = false;
}
}
public void drain(){
if((!empty) && boiled){
//排出巧克力动作
empty = true;
}
}
public void boil(){
if((!empty) && (!boiled)){
//煮沸
boiled = true;
}
}
}
上面代码使用单例模式实现了在使用巧克力工厂的时候,全局只有一个实例对象。
上面的经典单例模式按道理来说已经非常完美了,但是这里面存在一个隐藏的bug,那就是多线程问题。
我们知道线程是按照时间片来执行的,那么在单例模式里面存在一种极端的情况,那就是两个线程同时new uniqueInstance这个对象,当第一个线程执行到uniqueInstance = new SingletonChocolateFactory(); 的时候,时间片突然切换到第二个线程,第二个线程执行到uniqueInstance = new SingletonChocolateFactory();的时候,因为uniqueInstance这时候的指针是空的,所以会new一个uniqueInstance对象,当时间片再次切换到线程一的时候,会第二次new一个uniqueInstance对象,这样我们的单例模式就失效了。上述情况出现的时候,代码就会有bug,无法保证唯一性。
上面问题提供三种基本的解决办法,如下:
1. 最简单的解决办法就是在getInstance()方法上放一个同步锁synchronized。(解释一下同步锁,synchronized的含义就是,getInstance()方法只有当一个线程执行完以后,另一个线程才能执行,这样就保证了线程安全,也就保证了只有一个单例对象)。
具体代码如下:
public static synchronized SingletonChocolateFactory getInstance(){
if(uniqueInstance == null){
uniqueInstance = new SingletonChocolateFactory();
}
return uniqueInstance;
}
使用同步锁的弊端:虽然在原理是解决了这个问题,但是synchronized消耗资源挺多的,单例的这个类会经常调用getInstance()方法,所以耗资源挺严重。
2. “急切”创建实例,具体按照代码说明,如下:
public static SingletonChocolateFactory uniqueInstance = new SingletonChocolateFactory();
就是在uniqueInstance 开始位置直接创建对象,这样不管那个线程先执行,都会保证单例的唯一性。“急切”创建实例的弊端:这样做的不好处就是,有可能一次使用的过程中不会用到这个类的对象,但是会直接创建这个类的对象,这样就消耗了一部分的内存资源。
3. 双重检查加锁,这里是用到了Java中的volatile关键字,简单说明一下,volatile关键词是给编译器用的,就是为了处理多线程安全。如果不知道,就需要补习Java基础了。
public volatile static SingletonChocolateFactory uniqueInstance = null;
public static SingletonChocolateFactory getInstance(){
if(uniqueInstance == null){
synchronized (SingletonChocolateFactory.class){
if(uniqueInstance == null){
uniqueInstance = new SingletonChocolateFactory();
}
}
}
return uniqueInstance;
}
双重检查加锁是一种比较好的解决办法,具体使用上述三种的哪一种,要看每一个项目的实际使用环境来确定。