设计模式--单例模式

    说起单例设计模式,只要是学过编程的应该可以说是无人不知了,学习和了解设计模式也大多从单例开始,你可能会把单例模式的代码很流利的写出来,但你真的了解单例设计模式吗?下面我将通过问题的引入,一步一步带大家了解单例设计模式。

一、为什么要使用单例设计模式

我们知道,在java中,每次使用new关键字创建一个实例后,都会在堆内存开辟一块空间,你有没有想过对于某些类来说,多个实例往往是没有必要的,并且还有可能引发问题。举个例子来说,每台计算机可以连接若干个打印机,但只能有一个Printer Spooler(打印机后台处理程序),以避免两个打印作业同时输出到打印机中。再比如说windows中的资源管理器、回收站都是典型的单例,它们大都是进行资源管理服务,一方面它们没有存在多个的必要,另一个方面如果存在多个就可能导致问题的发生。

在java中,也有这样的一些“资源管理服务”的类,比如说线程池、数据库连接池。我们没必要让它们存在多个实例,白白浪费内存资源不说,更重要的是这样的类操作多个实例会引发安全问题。因此我们希望这一类的实例在应用程序中只存在一个就够了,正因如此,单例设计模式就应运而生了。单例模式的写法有很多种,全部列出来的话有七八种之多,这里只讲解最主流也是应用最广泛的两种写法---饿汉式和懒汉式。

二、单例设计模式的代码实现

在我给出代码实现前,首先有必要按照之前的叙述在脑海中构建一下单例应该怎么进行代码的实现,根据前边的描述,单例应该具有以下特点:

 

  • 类只能存在一个实例,因此不能随意的使用new关键字来创建该类的实例,在代码中的体现就是将构造器私有化。
  • 将构造器私有化后,我们若想得到该类的实例,就必须存在一个在程序中任何地方都能访问该类的访问点,并从该访问点获取该类的实例。体现在代码中就是存在这样一个静态方法,并且调用次方法能返回该类的唯一实例。

基于上述两点,我们脑海中就构造出了一个单例模式的模型,现在我们还应该知晓饿汉式和懒汉式的区别:

 

 

  • 饿汉式:在程序启动或单件模式类被加载的时候,单件模式实例就已经被创建。

 

 

 

  • 懒汉式:当程序第一次访问单件模式实例时才进行创建。

饿汉式

public class Single(){
     private static Single instance = new Single(); //程序启动时本类实例就会被创建       
     private Single(){
        //private修饰构造器,保证本类不能被创建实例
     }  
     public static Single getInstance(){ //通过静态函数使程序中任何地方都能获取到本类的实例
         return instance;
     } 
}

我们发现由于饿汉式单例模式是使用静态成员变量创建实例,因此在程序启动时该实例便会加载进内存,并且该成员变量instance被private修饰后保证该变量的引用无法被其他类获取,从而也就保证了该实例的唯一性。至此,饿汉式式单例就写好了,是不是很简单呢。但是现在我们要考虑一个问题,饿汉式的写法是不是完美的呢?它有没有什么缺点呢?

根据刚才描述我们知道,即使在应用中未曾使用到该类的实例,该类的实例也会随着类的加载而加载,这是不是对资源的一种浪费呢?是否有一种方法能判断一下我们是否需要该类的实例,如果需要才创建该类的实例,不需要则不会创建。那么懒汉式便能满足我们这个需求。

懒汉式

 

懒汉式第一版(线程不安全)

public class LazySingleton {
	
	private static LazySingleton instance = null;
	
	private LazySingleton(){
		
	}

	public static LazySingleton getInstance() {
		if (instance == null) { //判断instance是否为空,为空则说明之前未创建过该实例
			instance = new LazySingleton(); //创建本类实例
		}
		return instance; // 不为空则说明之前创建过本类实例,直接返回该实例。
	}
}

这样,我们就能通过判断该实例是否存在而决定何时创建实例了。诶?等等...不知道细心的小伙伴是否看出一些问题来了。如图所示:

当该实例尚未创建的时候,如果此时线程A判断以后进入了if中,然后线程A暂停执行,CPU又去调用线程B执行,由于线程A仅仅是刚进入if语句中,并未执行该实例的创建,因此B去判断的时候instance依然为null。这样就出麻烦了,线程A和线程B都会执行new LazySingleton();于是该类的实例便不再唯一。

这也是懒汉式单例模式的特征,会引发线程安全问题。解决这个麻烦,我们只要将引发线程安全的区域加个synchronized就好了,于是懒汉式第二版便产生了。

懒汉式第二版(解决了线程安全问题)

public class LazySingleton {
	
	private static LazySingleton instance = null;
	
	private LazySingleton(){
		
	}

	public static synchronized LazySingleton getInstance() { //使用synchronized函数保证线程安全
		if (instance == null) { //判断instance是否为空,为空则说明之前未创建过该实例
			instance = new LazySingleton(); //创建本类实例
		}
		return instance; // 不为空则说明之前创建过本类实例,直接返回该实例。
	}
}

如此,线程安全问题就解决了。但是我们仔细想想,这段代码是否完美了呢?我们知道只有当本类实例尚未创建的时候,多条线程才有可能同时进入if语句中创建多个实例,反之如果该实例已经存在了,那么线程到了if判断时候为null,那么它们根本就不会进入if语句。也就是说,引发线程安全问题的隐患仅仅发生在首次创建实例的那段时间里,而我们将synchronized加在函数上,即使实例创建完成后,每次调用该方法获取实例时都要加锁,其他线程都要进行等待,特别是程序中如果需要频繁创建该类的实例,那么这个性能的损耗实在是太大了,也是绝对不能接受的。

基于此,我们考虑能否将代码优化成仅仅在首次创建实例时对安全隐患进行同步,而创建实例后便不再对代码进行同步呢,由此懒汉式第三版便产生了

 

懒汉式第三版(解决了线程安全问题同时优化了性能)

public class LazySingleton {

	private static LazySingleton instance = null;

	private LazySingleton() {

	}

	public static LazySingleton getInstance() {
		if (instance == null) { //判断实例是否存在,已存在的话便可跳过synchronized直接返回该实例。
			synchronized (LazySingleton.class) {
				if (instance == null) { //线程未创建的时候,依然有可能多条线程同时进入外层的if。
					instance = new LazySingleton(); 
				}
			}
		}
		return instance; 
	}
}

外层的if是用来判断该类实例是否已经存在,存在的话直接跳过外层if从而返回该实例,从而也就避免了进入synchronized。但是当实例尚未创建时,依然可能有多条线程同时通过外层的if,如果没有内层的if进行判断,这些线程排队进入synchronized块后依然能创建多个实例,因此再synchronized中再进行一次判断。

至此,懒汉式单例模式可以说完美的解决了线程安全问题。但是如果你觉得上面这段代码已经很完美了,那就错了。重点来了,这段代码其实有个BUG,在这我就不通过引导的方式来阐述了,而是直接告诉大家,因为引发这个BUG不是我们代码写的有问题,而是由JVM引发的问题。

上面这种双重判断的写法,其实是从C语言中引申过来的,这种写法在C中有一个名词叫“双重检查成例”(以下简称为双重检查),在C的编译环境下,双重检查没有任何问题,并且在C中进行了非常广泛的应用,但是移植到Java当中,它是不成立的。其最大的原因就是JVM在编译时会对代码进行重排序优化,从而可能引起在首次创建该实例的过程中,当一条线程(这里称为线程A)初始化该类实例,但是并未完全初始化完成,但是由于指令重排序导致instance不再为null,那此时线程B判断外层if时便会直接跳出判断直接拿到该类的引用instance,当线程B拿着这个引用去调用该类的其他方法时,注意!!!此时线程A并未初始化完成,线程B只要一调用该类方法程序便会崩溃。总得来说,这就是由于JVM对程序进行重排序优化时导致的问题。这里我对该过程描述的较为简单,因为展开说要说的就太多了,如果有兴趣大家可以看那只老鼠小灰的专业讲解,非常详细和全面,因此我这里也就不班门弄斧了。漫画:什么是单例模式?

其实大家如果看过一些设计模式方面的书,会发现好多书给出的懒汉式单例的写法都是我上面的第三版,我在这里再强调一遍!这种写法是有缺陷的,虽说这种情况引起程序崩溃的概率微乎其微(我看过博客上有一个哥们为了实现这个BUG测了一天最后也没发生),但是并不代表我们就能将就着这样写,作为程序员我们的代码要尽量的严谨。说了些题外话,我们接着回到这段代码,那这个BUG究竟该怎么解决呢?在JDK1.5之前,这个问题可以说无解,如果你想提高性能,那么你就祈求那微乎其微的概率不要发生在你的程序上,否则你就老老实实的把synchronized加在函数上。但是在JDK1.5之后好消息出现了,这个问题随着volatile关键字的产生而得到了解决。volatile关键字有两个作用,其中之一就是禁止指令的重排序(另一个是保证可见性),因此在你使用双重检查的时候一定要记得将共享数据(即本类的成员变量instance)用volatile修饰。一般好消息的到来也一定会有一个坏消息随之而来。我们使用这种双重判断的写法其实就是为了提高程序的性能、降低CPU的损耗,但是使用volatile的坏消息是它会使性能降低,因为JVM的重排序机制本身就是为了性能的优化,当我们禁止重排序后理所当然会以性能下降为代价,但是至于使用volatile和把synchronized加在函数上,到底谁的性能损耗更大,我这里没有答案,也没有查找比较靠谱的答案,但是懒汉式的主流的写法大都是使用volatile。其实我想,究竟孰高孰低留给专业的人员去测试吧,这不是我们关心的,写这篇文章的目的在于想告诉大家双重判断中的这个坑。下面给出懒汉式单例的最终版本。

 

懒汉式单例最终版本

public class LazySingleton {

	private volatile static LazySingleton instance = null; //使用volatile修饰共享数据,从而禁止JVM重排序

	private LazySingleton() {

	}

	public static LazySingleton getInstance() {
		if (instance == null) { 
			synchronized (LazySingleton.class) {
				if (instance == null) { 
					instance = new LazySingleton(); 
				}
			}
		}
		return instance; 
	}
}

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值