`
bolide74
  • 浏览: 83324 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

Spring温故知新(三)singleton单例模式

阅读更多
本来今天打算介绍一下Spring的IoC容器,但是开了一早上的会感觉时间有点紧,今天写有点够呛。再加上看到昨天的访客大部分都对设计模式比较感兴趣,那么我就先提前介绍一下设计模式里也是比较重要的singleton单例模式。

      单例模式之所以提前在这里介绍,是由于Spring框架里的bean,或者说组件,获取实例的时候都是默认的单例模式,这是在多线程开发的时候要尤其注意的地方。

      那么什么是单例模式呢?我先引用一下别的地方抄来的理论介绍:
引用
单例模式的意思就是只有一个实例。单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。


这个理论介绍其实还是比较通俗易懂的,这里沿用我前面一篇IoC部分的造机器人的例子来解释。
      之前我们造机器人去向邻居打招呼,每次都是新造的一个机器人,向不同的邻居、或者不同的时间向同一个邻居打招呼都是要先新造一个机器人,然后派机器人去打招呼,接着招呼打完以后这机器人就被销毁了(机器人都沦为一次性产品,这得要多烧钱...),这就是这个机器人实例对象的“生存周期”。那么也就是说这个机器人类事实上是有很多个机器人实例对象的。
      我这么一解释,大家都会觉得这种打招呼的方法成本实在是很高。如果是低成本的小机器人可能还可以接受,但是如果是我想派我的Girlfriend实例对象去打招呼就不能接受了!难道每次打完招呼我就得跟这个GF分手然后再找一个?你丫这简直就不是成本问题了!这个是道德问题!
       在大型项目开发的时候,总会碰到一些在实例化对象的时候比较消耗资源的情况,比如反复读写同一个比较大的文件、或者反复链接数据库。这就好比它们是一个Girlfriend,每次要用到的时候再新造一个,用完再直接抛弃销毁是很不道德的事情!

       那么这时候单例模式就派上用场了,那么它是怎么实现让一个类只能生成一个实例呢?,下面举一个最简单的单例模式的示例(非线程安全):
Girlfriend类
package com.iteye.bolide74.action;

public class Girlfriend {
	private static Girlfriend girlfriend;  //类的静态成员属性形式声明一个实例

	public String name;
	public double height;
	public double weight;

	private Girlfriend(String name, double height, double weight) {
		this.name = name;
		this.height = height;
		this.weight = weight;
	}

	public static Girlfriend getGirlfriend() {
		if (girlfriend == null)
			girlfriend = new Girlfriend("GirlFriend", 1.64, 99.99);
		return girlfriend;
	}

	public void Speak(String msg) {
		System.out.println(msg + ",我是" + this.name + ",我是唯一的女朋友");
	}
}

大家可以看到这个Girlfriend类,跟前面一篇的静态工厂模式非常像,它们的构造函数都是private私有的,都有一个静态的实例生成方法。作用也是相同的,就是限制生成实例的办法只有一种,就是通过getGirlfriend()方法来获取。

区别有两点:
1) 静态工厂生成的实例对象{Robot2 robot2;},都是在getRobot()这个生产实例的方法内部声明的,也就是说这些引用对象都是局部变量。
   而单例模式里生成的实例对象{private static Girlfriend girlfriend;},是在getGirlfriend()这个生产实例的方法的外部声明,并且还加了static静态修饰词的,这就代表了它这个是类的静态成员属性,它是唯一的并且是无需实例化就能使用的(因为是static的),只不过这里又加了一个private私有修饰词,不能被外部直接访问而已。
2) Girlfriend类里的getGirlfriend()方法内部,加了一个判断,作用是如果girlfriend这个静态类属性,同时也是这个类的一个实例对象为null,那么就新建一个实例对象给它;如果不为null,也就是已经有实例了,那么就直接返回已经存在的那个实例对象。
引用
关于为什么这个类成员属性被设置为static静态的,就是唯一并且不需要实例化这个问题,这个涉及到内存机制,建议google一下相关知识。如果还是没搞懂的话请留言回复,我看到的话就会再抽空补充一下这个相关知识,如果这个不搞懂的话想要弄懂为什么单例模式是单例是不太可能的。

如果能搞懂以上部分,那么就能明白单例模式的实现原理了。它就是在获取某个类的实例对象的时候,限制它的获取方法必须为getInstance()方法,也就是我这里的getGirlfriend()。而getGirlfriend()这个方法呢,除了第一次调用是真正生成了一个新的实例以外,以后每一次调用其实都是返回了相同的一个实例对象。这就做到了一个类只能生成一个实例对象了。

用下面的一段实现类代码就可以测试一下效果:
package com.iteye.bolide74.tester;

import com.iteye.bolide74.action.Girlfriend;
import com.iteye.bolide74.action.Robot2;

public class Tester {
	public static void main(String[] args) {
		Robot2 robot1 = Robot2.getRobot(1);
		Robot2 robot2 = Robot2.getRobot(1);
		System.out.println(robot1 == robot2);
		//输出结果为false,也就是说两者是不同实例对象

		Girlfriend girlfriend1 = Girlfriend.getGirlfriend();
		Girlfriend girlfriend2 = Girlfriend.getGirlfriend();
		System.out.println(girlfriend1 == girlfriend2);
		//输出结果为true,两个引用对象 引用的是同一个实例对象
		girlfriend1.Speak("Hello,World!");
	}
}

小知识:“==”逻辑运算和equals方法是不同的,"=="是判断引用对象(也可以说是C里面的指针)是否引用了同一个实例对象(也就是是否指向同一个内存片段);而equals方法只是判断两个引用对象引用的实例对象的值是否是相等的,而不考虑是否是同一个内存片段,典型的示例:
		String str1="123";
		String str2=new String("123");
		System.out.println(str1==str2);   //false
		System.out.println(str1.equals(str2));   //true




      单例模式的基本原理我已经介绍完了,但是我要重点提醒一下,上面的Girlfriend类,并不是一个完善的单例模式,它只能起到一个介绍原理的作用。
      在单线程项目里使用这个Girlfriend单例类不会有太大问题,但是一旦涉及到并发多线程那么就会出很严重的问题了。

举个例子(举个栗子LoL):我家除了我在以外,还有我爹妈也都在,他们也都是宅人+使唤癖(物以类聚)。假设我们只能指挥我们家房间以内的东西。这时候我的Girlfriend还在门外(说明已经有了引用对象了,知道可以使唤Girlfriend)但是还没进房间(就是这个引用对象的实例还是null)。这时候我跟我爸妈就异口同声的瞬间同时(就先假设真正意义上的同时吧..)让我Girlfriend进门,然后去跟不同的邻居打招呼。
       在这种情况下,Girlfriend就会展现出超能力了,她会瞬间分裂出三个分身来听从我们三个人的命令,这就相当于原本只能是一个实例的,结果出来了三个不同的实例对象。
       这是因为原先单线程的情况下,我们一家三口是不会同时叫Girlfriend进门的,比如我已经把她叫进门了,我爸妈再想叫她的时候都会先判断她是不是已经在房间里了,如果已经在了,那当然是不用叫直接就使唤呗。
       但是在多线程情况下,三个人同时叫Girlfriend之前都是经过判断发现在那一瞬间Girlfriend确实是不在房间里的,所以理所当然的他们都会拖一个Girlfriend进门,结果Girlfriend就被迫影分身了。

       要解决并发多线程下的安全的单例模式,就得再在getGirlfriend方法上加上同步锁,同时内部判断是否为null的时候还要加上双重判断等等方法才能实现线程安全的单例模式。这个要解释的话就得涉及到并发多线程了,这里就暂时不做解释。有兴趣的可以翻一下其他的文章。


       另外由于java的反射机制,还有一种方法可以破解安全的单例模式,那就是直接把现有的实例序列化,然后再克隆一个序列化的实例,再通过Classloader之类的方法把它反序列化。大概原理似乎是这样,以前看到过相关文章但是没去仔细看过,就是知道有这么一个方法,有兴趣的话可以去google一下。
       我提出这么一个事情,也就是提醒大家单例模式也不是真的能完全让一个实例唯一存在,总会有那么些突破的方法,所以一定要注意。


下一篇:Spring温故知新(四)用HashMap写一个自己的Spring IoC简易容器吧!http://bolide74.iteye.com/blog/1002610
上一篇:Spring温故知新(二) IoC控制反转与DI依赖注入http://bolide74.iteye.com/blog/998650









6
3
分享到:
评论
7 楼 sh305520891 2011-05-06  
你的单粒太粗糙了!
我给你随手写个吧(饿汉式)
public class SingleTon {
    private static SingleTon instance = new SingleTon();
    
    //  单粒模式构造
    private SingleTon() {}
    
     public sychronized static SingleTon getInstance() {
         return instance;
     }
}
6 楼 sh305520891 2011-05-06  
public class SingleTon {
   private static SingleTon instance = new SingleTon();

}
5 楼 C.T 2011-04-29  
lz这种是懒汉单列模式,当有需要的时候才被创建出来,还有另一种是饿汉单列模式,就是在一开始就创建对象,而不允许对象再次被创建,可以作为一个补充吧,当然还有线程安全的问题,线程安全的单列模式也有几种的实现方式。
4 楼 bolide74 2011-04-24  
ericslegend 写道
关于单例的各种情况处理,楼主可以看这个http://www.iteye.com/topic/575052

嗯,这篇博文是一个很好的并发情况下的单例模式的比较详细补充,大家有需要的话也都可以看一下
3 楼 ericslegend 2011-04-23  
关于单例的各种情况处理,楼主可以看这个http://www.iteye.com/topic/575052
2 楼 bolide74 2011-04-13  
javaeye 写道
1) 静态工厂生成的实例对象{Robot2 robot2;},都
这句话好像有些问题。按我的理解,对象是保存在堆空间,是全局性的,只要获得了这个对象的引用,就可以访问该对象。
如果说robot2是局部变量,那也就无法返回。

嗯,是我没说清楚产生的误解。事实上robot2这个是局部的引用对象(指针变量),它的生存周期只存在于这个方法以内,方法调用完以后,robot2就被销毁了。但是由于robot2销毁之前被return到调用这个方法的引用对象上了(可能是复制了一个引用对象),所以robot2所引用的值,也就是new出来的那个实例对象并没有由于没被任何对象引用就被销毁了,因为它被外部调用这个方法的引用对象给引用了。
简单的说就是robot2作为一个局部的指针变量已经被销毁了,但是由于它销毁前被return复制了一份给了外部的指针,所以robot2指针指向的那块内存,也就是new Robot2()这个实例对象还是没被回收,依然存在

不知道这么解释是不是对的?
1 楼 jupiterful 2011-04-13  
javaeye 写道
1) 静态工厂生成的实例对象{Robot2 robot2;},都是在getRobot()这个生产实例的方法内部声明的,也就是说这些实例对象都是局部变量。


这句话好像有些问题。按我的理解,对象是保存在堆空间,是全局性的,只要获得了这个对象的引用,就可以访问该对象。
如果说robot2是局部变量,那也就无法返回。

楼主写的不错,比喻很生动,很有启发。

相关推荐

Global site tag (gtag.js) - Google Analytics