设计模式:单例模式
一、前言
单例模式比较简单,可以说没有复杂的调用和接口的设计,就是一个简单的类,只是要求这个类只能生成一个对象,无论是什么时候都要保证这一点,因此只能生成一个实例的模式就叫做单例模式。这是一种新的思想,在前面我们学过了迭代器和适配器的适配思想;模板方法和工厂方法的交给子类思想;现在我们学习一下生成实例的思想。
二、导引和代码
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 = "朱彦荣";
三、总结
在这个单例模式中,我希望大家不要只知道单例的思想,更要知道类的加载和初始化时机,以及多线程的机制,我想这才是真正有意义的呢。