设计模式:单例模式

设计模式:单例模式

一、前言

  单例模式比较简单,可以说没有复杂的调用和接口的设计,就是一个简单的类,只是要求这个类只能生成一个对象,无论是什么时候都要保证这一点,因此只能生成一个实例的模式就叫做单例模式。这是一种新的思想,在前面我们学过了迭代器和适配器的适配思想;模板方法和工厂方法的交给子类思想;现在我们学习一下生成实例的思想。

二、导引和代码

  2.1、什么时候类被加载和初始化?

   类的加载是通过类加载器(Classloader)完成的,它既可以是饿汉式加载类,也可以是懒汉式加载,这跟不同的JVM实现有关。加载完类后,类的初始化就会发生,如果是对一个类的主动使用就会初始化对象,对类的被动使用不会对类进行初始化,比如final修饰的静态变量如果能在编译时就确定变量的取值,会被当做常量,作为对一个类的被动使用不会导致类的初始化以下情况类被初始化

    最常见的就是直接创建一个类的实例(new 一个对象),有可能导致ClassNotFoundException。
    调用一个类的静态方法,public static XXX();
    调用一个类的静态变量并且它不是常量或者是对静态变量进行赋值;
    调用Java API的反射,例如动态加载一个类Class.forName();
    初始化一个类的子类会首先初始化一个类的父类;
    启动包含main()的类;
    在顶层类中执行assert语句;

  类初始化的一些规则:

    类从顶至底的顺序初始化,所以声明在顶部的字段的早于底部的字段初始化;
    超类早于子类和衍生类的初始化;
    如果类的初始化是由于访问静态域而触发,那么只有声明静态域的类才被初始化,而不会触发超类的初始化或者子类的初始化,即使静态域被子类或子接口或者它的实现类所引用;
    接口初始化不会导致父接口的初始化;
    静态域的初始化是在类的静态初始化期间,非静态域的初始化时在类的实例创建期间,这意味着静态域初始化在非静态域之前;
    非静态域通过构造器初始化,子类在做任何初始化之前构造器会隐含地调用父类的构造器,他保证了父类非静态或实例变量初始化早于子类;

  2.2、单例代码

Singleton类:

 1 package zyr.dp.singleton;
 2 
 3 public class Singleton {
 4    private static Singleton singleton=new Singleton();
 5    private Singleton(){
 6        System.out.println("开始初始化对象...");
 7    }
 8    public static Singleton getInstance(){
 9        return singleton;
10    }
11 }

Main函数:

 1 package zyr.dp.singleton;
 2 
 3 public class Main {
 4 
 5     public static void main(String[] args) {
 6      
 7         Singleton object1= Singleton.getInstance();
 8         Singleton object2= Singleton.getInstance();
 9         if(object1==object2){
10              System.out.println("是同一个对象...");
11         }else{
12              System.out.println("不是同一个对象...");
13         }
14         
15     }
16     
17 }

运行结果:

下面我们对问题进行深入研究,

  2.2.1、将单例模式的定义改一下:

1  public  static final Singleton singleton=new Singleton();

  变成公有的,便于我们在外面调用,并且是final类型的,那就必须赋值了,这是一个怪异的组合,虽然满足了静态final类型但是却不能在编译的时候初始化,因为后面用到了new关键字,因此在这个时候就会初始化,我们将Main改成如下所示:

 1         Singleton object1;
 2         System.out.println(Singleton.singleton+"-------------");
 3         
 4         object1= Singleton.getInstance();
 5         System.out.println("-------------");
 6         Singleton object2= Singleton.getInstance();
 7         System.out.println("-------------");
 8         if(object1==object2){
 9              System.out.println("是同一个对象...");
10         }else{
11              System.out.println("不是同一个对象...");
12         }

运行结果:

  可以看到确实如我们想要的结果,在静态部分初始化了。

  2.2.2、我们将Singleton代码恢复成原样,在main中改成如下所示,看一看到如果我们只是定义了一个变量,或者将其赋值为null,类初始化是不会开始的,只有我们调用了静态方法的时候才开始初始化。

 1         Singleton object1;
 2         object1=null;
 3         System.out.println("-------------");
 4         object1= Singleton.getInstance();
 5         System.out.println("-------------");
 6         Singleton object2= Singleton.getInstance();
 7         System.out.println("-------------");
 8         if(object1==object2){
 9              System.out.println("是同一个对象...");
10         }else{
11              System.out.println("不是同一个对象...");
12         }

结果:

  2.2.3、然后我们再将Singleton添加一个字段:

1    public static  int sum=100;

然后main中:

 1         System.out.println(Singleton.sum);
 2         System.out.println("-------------");
 3 
 4         Singleton object1;
 5         object1=null;
 6         System.out.println("-------------");
 7         object1= Singleton.getInstance();
 8         System.out.println("-------------");
 9         Singleton object2= Singleton.getInstance();
10         System.out.println("-------------");
11         if(object1==object2){
12              System.out.println("是同一个对象...");
13         }else{
14              System.out.println("不是同一个对象...");
15         }

结果:

  2.2.4、最后我们将sum变量改成:

1    public static final int sum=100;

结果:

可以看到确实是如果有final的常量不会触发类加载器,使用的时候可以看做常量,而没有加final的变量,一旦使用就会先初始化。

   由此得出一个深刻的结论,我们的对象初始化的时间是Singleton object1= Singleton.getInstance();中的Singleton.getInstance(),当这个方法执行的时候,要做这几件事情,首先就是看一下自己有没有父亲,如果有先初始化父亲的静态变量,然后初始化自己的静态变量private  static  Singleton singleton=new Singleton();之后初始化父类的构造器中的变量,最后初始化自己构造器之中的方法,到了最后一切进行完毕,才开始调用Singleton.getInstance()方法,这个时候singleton早就有对象了,因此将对象返回。因此当打印出“开始初始化对象”的时候(执行自身构造器方法),其实作为静态变量的singleton早就已经初始化成功了,这里有一定的误导之嫌,望读者注意。那么到底要不要在private  static  Singleton singleton=new Singleton();中加final呢?其实理解到这种程度我们就知道无所谓了,因为是私有,只有自己能使用,定义final想把自己当做编译时就能处理的常量,可是又使用了new关键字,就失去了这个作用了,因此有和没有一点关系都没有,当然这是不考虑反射的时候。

   2.3、那么在有的教材中还有一些单例方法,比如称我们这里的方法为饿汉式方法,而其他的又懒汉式方法,懒汉式方法总是会出现这样或那样的问题的,因为考虑到了多线程机制,实现起来比较麻烦,并且还会出现问题,就算是使用了一定的解救办法(同步、加锁、双重判断)的办法,性能还是被损耗了,因此懒汉式方法的弊端非常大,但是因为‘懒’,所以可以晚一点加载,占用的内存就会晚一点,这样可以为之前的进程节省空间,这也是从辩证法角度看问题的必然结果,凡事有利必有弊。我们简单看一下吧:

 1 package zyr.dp.singleton;
 2 
 3 public class SingletonLazy {
 4    private  static  SingletonLazy singletonLazy=null;
 5 
 6    private SingletonLazy(){
 7        System.out.println("开始初始化对象...");
 8    }
 9    public static SingletonLazy getInstance(){
10        if(singletonLazy==null){
11            return new SingletonLazy();
12        }
13        return singletonLazy;
14    }
15 }

  上面的代码就是错误的,如果产生多线程,那么就有可能全部进入if(singletonLazy==null)的逻辑之中,最后产生很多的SingletonLazy()对象。我们可以使用加锁机制,加入关键字synchronized就可以了,这个关键字的内部逻辑其实就是临界区的概念,特别的浪费CPU性能。

1    public static synchronized SingletonLazy getInstance(){
2        if(singletonLazy==null){
3            return new SingletonLazy();
4        }
5        return singletonLazy;
6    }

  或者使用如下方式,双重判断,第二次判断就是防止已经有一个对象产生了,因此也可以达到相应的目的。

 1    public static  SingletonLazy getInstance(){
 2        if (singletonLazy == null) {    
 3            synchronized (Singleton.class) {    
 4               if (singletonLazy == null) {    
 5                   singletonLazy = new SingletonLazy();   
 6               }    
 7            }    
 8        }    
 9        return singletonLazy;
10    }

  当然还有其他补救措施,在这里不一一列举。

  2.4、一个例子

Parent类:

 1 package zyr.dp.classloader;
 2 
 3 public class Parent {
 4 
 5     //protected static String familyName = "朱彦荣";
 6     
 7     public  Parent(){
 8         System.out.println("父类构造器初始化..."); 
 9     }
10     
11     static { 
12         System.out.println("父类 静态代码块 被初始化"); 
13     }
14     { System.out.println("父类 非静态代码块 被初始化");}
15 }

Child类:

 1 package zyr.dp.classloader;
 2 
 3 public class Child extends Parent{
 4 
 5     public  Child(){
 6         System.out.println("子类构造器初始化..."); 
 7     }
 8     
 9     static { 
10         System.out.println("子类 静态代码块 被初始化"); 
11     }
12     
13     {System.out.println("子类 非静态代码块 被初始化");}
14     
15 }

Main:

 1 package zyr.dp.classloader;
 2 
 3 public class Main {
 4 
 5     public static void main(String[] args) {
 6         Child child = new Child(); 
 7         //System.out.println(Child.familyName);
 8     }
 9 
10 }

结果:

在父类中加入:

1 protected static String familyName = "朱彦荣";

main:

 1 package zyr.dp.classloader;
 2 
 3 public class Main {
 4 
 5     public static void main(String[] args) {
 6         //Child child = new Child(); 
 7         System.out.println(Child.familyName);
 8     }
 9 
10 }

结果:(子类一点也没有被初始化,父类被初始化)

改成:(常量,编译时决定)

1 protected static final String familyName = "朱彦荣";

三、总结

  在这个单例模式中,我希望大家不要只知道单例的思想,更要知道类的加载和初始化时机,以及多线程的机制,我想这才是真正有意义的呢。

  程序代码

posted @ 2018-06-25 16:13  精心出精品  阅读(884)  评论(2编辑  收藏  举报