设计模式之单例模式

在所有的设计模式里面,单例模式算是最简单的了。在这里先说说单例模式在哪些情况下使用了?

在实际开发中,有些对象我们只需要一个:线程池、缓存、硬件设备等;如果多个实例会造成有冲突、结果的不一致性等问题,都可以使用单例模式来解决。那么有人会说,不是用单例模式也是可以的,比如说:可以用静态变量方式来实现,或者程序员之间协商一个全局变量,有好多种解决方案。我们为什么要使用单例模式了?单例模式作为前人经验的总结,在实现以及时间复杂度上面,都是比较优秀的解决方案。其他解决办法有可能灵活度不够,而且每个程序员写的代码的质量是不一样的,这样时间复杂度也是一个不要控制的因素。

什么是单例模式?

单例模式就是确保一个类最多只有一个实例,并提供一个全局访问点。

单例模式类图如下:


单例模式代码实现如下:

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;
	}

双重检查加锁是一种比较好的解决办法,具体使用上述三种的哪一种,要看每一个项目的实际使用环境来确定。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程之艺术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值