Quantcast
Channel: CSDN博客推荐文章
Viewing all articles
Browse latest Browse all 35570

设计模式学习之路---单例模式(二)

$
0
0

            作者:zuoxiaolong8810(左潇龙),转载请注明出处。

            上一章,我们学习了设计模式的概念,以及为什么要学习设计模式,还有在进行系统设计时应当遵守的六大原则,本章我们就来开始一一的学习GOF当中的二十三钟设计模式。

            我一会在思考如何去诠释这么多设计模式,因为网上有很多现成的,可供学习的资料,我在想有什么地方可以让各位跟着我的节奏去学习,而不是网上的那些资料,优势在哪里,思考很久,我觉得唯一的优势,或者说我能有的优势,就是简单通俗易懂。

            遵循着中心思想通俗易懂,我们首先来回顾一下单例模式为何要出现,又或者说什么样的类可以做成单例的。

            在我的工作过程中,我发现所有可以使用单例模式的类都有一个共性,那就是这个类没有自己的状态,换句话说,这些类无论你实例化多少个,其实都是一样的。

            我稍微总结一下,一般最容易区别的地方就在于,这些类,都没有非静态的,可设置值的属性。

            我来举个反面教材,即一个不能被做成单例模式的类。

public class NoSingleton {

	private int changedField;
	
	private static int staticChangedField;
	
	private void setChangedField(int value){
		this.changedField = value;
	}
	
	private static void setStaticChangedField(int value){
		staticChangedField = value;
	}
	
}
             上面这个类,有两个属性,一个是静态的,一个是非静态的,而且非静态的属性我们提供了set方法(暂且不考虑突破属性访问权限的反射机制),那么这个类就是具有自己的状态的,因为它有一个实例级别的变量changedField,这个变量在每个实例当中都有可能是不一样的。而静态的则不会有这种担忧,因为它是被这个类的所有实例共享的。

             所以上述这个类不能做成单例的类,一般可以做成单例的类的样子都有以下特点。

             1.所有的对外可变属性都是静态的。

             2.所有的非静态属性都是私有的,并且没有对外公开的可以设置值的方法。

             下面,我们就来看一下做成单例的几种方式。

             首先,我们来看一下最标准也是最原始的单例模式的构造方式。

public class Singleton {

	//一个静态的实例
	private static Singleton singleton;
	//私有化构造函数
	private Singleton(){}
	//给出一个公共的静态方法返回一个单一实例
	public static Singleton getInstance(){
		if (singleton == null) {
			singleton = new Singleton();
		}
		return singleton;
	}
}
            这是在不考虑并发访问的情况下标准的单例模式的构造方式,这种方式通过几个地方来限制了我们取到的实例是唯一的。

            1.静态实例,带有static关键字的属性在每一个类中都是唯一的,此为保证单例的最重要的一步。

            2.限制客户端随意创造实例,即私有化构造方法。

            3.给一个公共的获取实例的静态方法,注意,是静态的方法,因为这个方法是在我们未获取到实例的时候就要提供给客户端调用的,所以如果是非静态的话,那就变成一个矛盾体了,因为非静态的方法必须要拥有实例才可以调用。

            4.判断只有持有的静态实例为null时才调用构造方法创造一个实例,否则就直接返回。

            假如你去面试一家公司,给了你一道题,让你写出一个单例模式的例子,那么如果你是刚出大学校门的学生,你能写出上面这种示例,假设我是面试官的话,满分100的话,我会给90分,剩下的那10分算是给更优秀的人一个更高的台阶。但如果你是一个有过两三年工作经验的人,如果你写出上面的示例,我估计我最多给你30分,甚至心情要是万一不好的话可能会一分不给。

           为什么同样的示例放到不同的人身上差别会这么大,就是因为前面我提到的那个情况,在不考虑并发访问的情况下,上述示例是没有问题的。

           至于为什么在并发情况下上述的例子是不安全的呢,我在这里给各位制造了一个并发的例子,用来说明,上述情况的单例模式,是有可能造出来多个实例的,我自己测试了约莫100次左右,最多的一次,竟然造出了3个实例。下面给出代码,大约运行10次(并发是具有概率性的,10次只是保守估计,也可能一次,也可能100次)就会发现我们创造了不只一个实例。

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestSingleton {
	
	boolean lock ;
	
	public boolean isLock() {
		return lock;
	}

	public void setLock(boolean lock) {
		this.lock = lock;
	}
	
	public static void main(String[] args) throws InterruptedException {
		final Set<String> instanceSet = Collections.synchronizedSet(new HashSet<String>());
		final TestSingleton lock = new TestSingleton();
		lock.setLock(true);
		ExecutorService executorService = Executors.newCachedThreadPool();
		for (int i = 0; i < 100; i++) {
			executorService.execute(new Runnable() {
				
				public void run() {
					while (true) {
						if (!lock.isLock()) {
							Singleton singleton = Singleton.getInstance();
							instanceSet.add(singleton.toString());
							break;
						}
					}
				}
			});
		}
		Thread.sleep(5000);
		lock.setLock(false);
		Thread.sleep(5000);
		System.out.println("------并发情况下我们取到的实例------");
		for (String instance : instanceSet) {
			System.out.println(instance);
		}
		executorService.shutdown();
	}
}
               我在程序中同时开启了100个线程,去访问getInstance方法,并且把获得实例的toString方法获得的实例字符串装入一个同步的set集合,set集合会自动去重,所以看结果如果输出了两个或者两个以上的实例字符串,就说明我们在并发访问的过程中产生了多个实例。

               程序当中让main线程睡眠了两次,第一次是为了给足够的时间让100个线程全部开启,第二个是将锁打开以后,保证所有的线程都已经调用了getInstance方法。

               好了,这下我们用事实说明了,上述的单例写法,我们是可以创造出多个实例的,至于为什么在这里要稍微解释一下,虽说我一直都喜欢用事实说话,包括看书的时候,我也不喜欢作者跟我解释为什么,而是希望给我一个例子,让我自己去印证。

              造成这种情况的原因是因为,当并发访问的时候,第一个调用getInstance方法的线程,在判断完singleton是null的时候,就进入了if块准备创造实例,但是同时另外一个线程在第一个调用的线程还未创造出来之前,就又进行了singleton是否为null的判断,这时singleton依然为null,所以第二个线程也会进入if块去创造实例,这时问题就出来了,有两个线程都进入了if块去创造实例,结果就造成单例模式并非单例。

              为了避免这种情况,我们就要考虑并发的情况了,我们最容易想到的方式应该是下面这样的方式。

public class BadSynchronizedSingleton {

	//一个静态的实例
	private static BadSynchronizedSingleton synchronizedSingleton;
	//私有化构造函数
	private BadSynchronizedSingleton(){}
	//给出一个公共的静态方法返回一个单一实例
	public synchronized static BadSynchronizedSingleton getInstance(){
		if (synchronizedSingleton == null) {
			synchronizedSingleton = new BadSynchronizedSingleton();
		}
		return synchronizedSingleton;
	}
	
}
               上面的做法很简单,就是将整个获取实例的方法同步,这样在一个线程访问这个方法时,其它所有的线程都要处于挂起等待状态,倒是避免了刚才同步访问创造出多个实例的危险,但是我只想说,这样的设计实在是糟糕透了,这样会造成很多无谓的等待,所以为了表示我的愤怒,我在类名上加入Bad。

               其实我们同步的地方只是需要发生在单例的实例还未创建的时候,在实例创建以后,获取实例的方法就没必要再进行同步控制了,所以我们将上面的示例改为很多教科书中标准的单例模式版本,也称为双重加锁。

public class SynchronizedSingleton {

	//一个静态的实例
	private static SynchronizedSingleton synchronizedSingleton;
	//私有化构造函数
	private SynchronizedSingleton(){}
	//给出一个公共的静态方法返回一个单一实例
	public static SynchronizedSingleton getInstance(){
		if (synchronizedSingleton == null) {
			synchronized (SynchronizedSingleton.class) {
				if (synchronizedSingleton == null) {
					synchronizedSingleton = new SynchronizedSingleton();
				}
			}
		}
		return synchronizedSingleton;
	}
}
                这种做法与上面那种最无脑的同步做法相比就要好很多了,因为我们只是在当前实例为null,也就是实例还未创建时才进行同步,否则就直接返回,这样就节省了很多无谓的线程等待时间,值得注意的是在同步块中,我们再次判断了synchronizedSingleton是否为null,解释下为什么要这样做。

               假设我们去掉同步块中的是否为null的判断,有这样一种情况,假设A线程和B线程都在同步块外面判断了synchronizedSingleton为null,结果A线程首先获得了线程锁,进入了同步块,然后A线程会创造一个实例,此时synchronizedSingleton已经被赋予了实例,A线程退出同步块,直接返回了第一个创造的实例,此时B线程获得线程锁,也进入同步块,此时A线程其实已经创造好了实例,B线程正常情况应该直接返回的,但是因为同步块里没有判断是否为null,直接就是一条创建实例的语句,所以B线程也会创造一个实例返回,此时就造成创造了多个实例的情况。

              走到现在,上述双重加锁的示例看起来貌似是没有问题了,但其实仍然是有问题的,因为虚拟机在执行创建实例的这一步操作的时候,其实是分了好几步去进行的,也就是说创建一个新的对象并非是原子性操作。在有些JVM中上述做法是没有问题的,但是有些情况下是会造成莫名的错误。

              首先要明白在JVM创建新的对象时,主要要经过三步。

              1.分配内存

              2.初始化构造器

              3.将对象指向分配的内存的地址

              这种顺序在上述双重加锁的方式是没有问题的,因为这种情况下JVM是完成了整个对象的构造才将内存的地址交给了对象。但是如果2和3步骤是相反的,就会出现问题了。

              因为这时将会先将内存地址赋给对象,针对上述的双重加锁,就是说先将分配好的内存地址指给synchronizedSingleton,然后再进行初始化构造器,这时候后面的线程去请求getInstance方法时,会认为synchronizedSingleton对象已经实例化了,直接返回一个引用。如果在初始化构造器之前,这个线程使用了synchronizedSingleton,就会产生莫名的错误。

             所以我们在语言级别无法避免错误的发生,我们只有将该任务交给JVM,所以有一种比较标准的单例模式。如下所示。

package com.oneinstance;

public class InnerClassSingleton {
	
	public static Singleton getInstance(){
		return Singleton.singleton;
	}

	private static class Singleton{
		
		protected static Singleton singleton = new Singleton();
		
	}
}
              首先来说一下,这种方式为何会避免了上面莫名的错误,众所周知,一个类的静态属性只会在第一次加载类时会被初始化,这是JVM级别的限制,所以我们无需担心初始化进行一半的时候,有人使用了这个实例,而且由于静态变量只初始化一次,所以singleton仍然是单例的。另外我们也不需要担心并发所产生的问题,因为JVM会帮我们保证初始化只进行一次。上面这种写法是我们使用内部类作为单例,这样不太符合我们的习惯。我们改为以下形式。

public class Singleton {
	
	private Singleton(){}
	
	public static Singleton getInstance(){
		return SingletonInstance.instance;
	}
	
	private static class SingletonInstance{
		
		static Singleton instance = new Singleton();
		
	}
}
             好了,进行到这里,单例模式算是已经完成了。最终的产物就是如上述的形式。上述形式保证了以下几点。

             1.Singleton最多只有一个实例,在不考虑反射强行突破访问限制的情况下。

             2.保证了并发访问的情况下,不会发生由于并发而产生多个实例。

             3.保证了并发访问的情况下,不会由于初始化动作未完全完成而造成使用了尚未正确初始化的实例。

             另外,我在这里再给出几种不太常用的单例的构造方式,给出之后我会简单的说下这种方式的优缺点。

public class Singleton {
	
	private static Singleton singleton = new Singleton();
	
	private Singleton(){}
	
	public static Singleton getInstance(){
		return singleton;
	}
	
}
              上述方式与我们最后一种给出的方式类似,只不过没有经过内部类处理,这种方式最主要的缺点就是一旦我访问了Singleton的任何其他的静态域,就会造成实例的初始化,而事实是可能我们从始至终就没有使用这个实例,造成内存的浪费。

              还有一种方式我就不贴了,与双重锁定一模一样,只是给静态的实例属性加上关键字volatile,标识这个属性是不需要优化的。这样也不会出现实例化发生一半的情况,因为加入了volatile关键字,就等于禁止了JVM自动的指令重排序优化,并且强行保证线程中对变量所做的任何写入操作对其他线程都是即时可见的。这里没有篇幅去介绍volatile以及JVM中变量访问时所做的具体动作,总之volatile会强行将对该变量的所有读和取操作绑定成一个不可拆分的动作。如果读者有兴趣的话,可以自行去找一些资料看一下相关内容。

             不过值得注意的是,volatile关键字是在JDK1.5之后才被给予了意义,所以这种方式要在JDK1.5之后才可以使用。

             好了,以上基本上就是常见的所有单例模式的构造方式,如果下次再有面试让你去写一个单例模式,有时间的话就把上面所有的全部写给面试官并一一将优劣讲给他听吧,这样的话估计offer已经离你不远了。

             下期预告,代理模式。

              




             

           



作者:zuoxiaolong8810 发表于2013-6-2 21:15:03 原文链接
阅读:10 评论:0 查看评论

Viewing all articles
Browse latest Browse all 35570

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>