黑马程序员技术交流社区

标题: 论单例模式的简易编写姿势(大涨姿势啊) [打印本页]

作者: 龙骑将杨影枫    时间: 2014-12-14 23:10
标题: 论单例模式的简易编写姿势(大涨姿势啊)
首先说明,此法并非我本人原创,而是转载。原文地址:http://devbean.blog.51cto.com/448512/203501/。我只是说说我受到的启发。
当然了,在此之前,首先解释一下什么是单例模式。
所谓单例模式,就是包含以下特征:
1、单例类只能有一个实例。
2、单例类必须自己自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。

如果太抽象,没关系,举一个很现实的例子。
Calendar日期类,这个类是不能        Calendar cl=new Calendar()这么写的,IDE会报错,不用IDE写虽然不报错,但是也肯定编译不过去。因为这个类主要是获得系统时间,是一个常量(相对而言)。系统里的时间是只能有一个的(否则一个上午七点一个下午七点就不知道该看哪一个了),符合第一条;Calendar类的正确实例化方式是Calendar cl=Calendar.getInstance(),其中getInstance()是一个静态方法,可以通过函数名直接调用,符合第三条;然后getgetInstance()方法里是这么写的:
  1.     public static Calendar getInstance()
  2.     {
  3.         Calendar cal = createCalendar(TimeZone.getDefaultRef(), Locale.getDefault(Locale.Category.FORMAT));
  4.         cal.sharedZone = true;
  5.         return cal;
  6.     }
复制代码
自己创建了唯一的实例,符合2.因此Calendar类是依循单例模式设计的。
好吧,那么如何实现单例模式呢?
简单的写法是这样:
  1. package singleton;

  2. public class Singleton1 {
  3.         //创建一个实例
  4.         private static final Singleton1 s1=new Singleton1();
  5.         //必须是自己创建这个实例,别人无法调用其构造方法
  6.         private Singleton1(){
  7.         }
  8.         //必须自己创建这个实例供其他人调用
  9.         public static Singleton1 getInstance(){
  10.                 return s1;
  11.         }
  12. }
复制代码
但是这个方法有个缺点,就是一旦私有的构造方法里有调用很大资源的东西(例:读取大量数据并进行分析比对),而这个类又被用的比较散(或者干脆没用到),那么这么个设计是很浪费资源的。因为类的实例化是有static修饰,所以不管用不用都的有这么个占内存的玩意儿。
那么大家会说,我一开始置空,需要的时候在实例化。然后一直调用这个实例可以嘛?
  1. package singleton;

  2. public class Singleton2 {
  3.         //创建实例,但是首先置空
  4.         private static Singleton2 s2=null;
  5.         //必须是自己创建这个实例,别人无法调用其构造方法
  6.         private Singleton2(){
  7.         }
  8.         //必须自己创建这个实例供其他人调用
  9.         public static Singleton2 getInstance(){
  10.                 //运行时进行检测
  11.                 //如果是首次加载,则初始化实例
  12.                 //如果是以后加载,则调用已实例
  13.                 if(s2==null){
  14.                         s2=new Singleton2();
  15.                 }
  16.                 return s2;
  17.         }
  18. }
复制代码
这种情况,看上去,是对的,实际上,是错的。因为这只是在单线程下可以保证正确运行,如果是多线程。假设A线程调用了此类的实例化方法,由于此前没调用,所以Singleton2 s2是空值,那么A线程就下达了实例化的指令。然后,此时B线程好死不死的冲过来抢了A线程的资源,所以A线程靠边站,B线程来执行实例化操作。此时因为A是处于将实例化还是没有实例化的阶段,所以Singleton2 s2还是空值,所以B线程也会发出进行实例化的指令,完成实例化。然后资源再分配给A,此时Singleton2 s2已经被B实例化过了,实际上已经不是空值。而A因为之前检测过了,没有再回头看一眼的情况下,也会继续按照“Singleton2 s2是空值”这个错误的前提去进行操作,又实例化出一个对象s2,造成了同一系统中有两个实例的坑爹情况。所以要加各种synchronized用来防止抢线程出现。具体过程比较坑,有兴趣的可以研究。 我来解释下高明的做法:
  1. package singleton;
  2. public class Singleton5 {
  3.         private Singleton5(){
  4.                 System.out.println("==test");
  5.         }
  6.         //调用私有类的静态变量
  7.         public static Singleton5 getInstance(){
  8.                 return Singleton5Instance.instance;
  9.         }
  10.         //创建内部私有类
  11.         private static class Singleton5Instance{
  12.                 private static final Singleton5 instance=new Singleton5();
  13.         }
  14. }
复制代码
这种写法高明就高明在,通过增加一个内部类,完美规避了以上提出的2点障碍。
1 占资源。
虽然内部类是用static修饰的,但是外部类是完完全全的原生态。也就是说,不动外部类,内部类是不会参与到系统资源分配的。
2 线程安全
这种写法高明就高明在,完全把线程安全用私有类的构造解决了。换句话说,要实例化Singleton5 ,必先调用getInstance;要调用getInstance方法,必先构造内部类。但是构造方法是完全不可分割的。换句话说不会有线程A构造了一半资源被B抢走的情况。
写到这里我想大呼一声:给大爷跪了!

可能这个例子很小很小,也没什么大不了。但是对我来说,这种闪耀着创新以及智慧的光芒就是我追寻的目标啊!

所以说编程是多么有意的事情,同意的请举手。






作者: 提米特    时间: 2014-12-15 09:12
很有意思,感谢楼主的分享
作者: Elvismao    时间: 2014-12-15 09:55
内部类的完美应用,厉害!
作者: 逆世界ylm    时间: 2014-12-15 10:22
楼主威武,学习了




欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2