设计模式之单例模式的几个问题(1)
单例对象的初始化时机:
上篇博文设计模式之单例模式给出了5种单例模式的实现方法,其中静态代码块与饿汉模式的本质一致,都归为饿汉模式。其中饿汉模式和枚举方式都属于立即加载,懒汉式和静态代码块属于延时加载。如何理解立即加载和延时加载,需要从类加载机制聊一下。
Java虚拟机的类加载过程主要有七个步骤:Loading、verification、preparation、resolution、initialization、using、unloading。翻译中文就是:加载,验证,准备,解析,初始化,使用和卸载。
类什么时候加载:
类的加载是通过类加载器(Classloader)完成的,它既可以是立即加载[eagerly load](只要有其它类引用了它就加载)加载类,也可以是延时[lazy load](等到类初始化发生的时候才加载),由不同的JVM实现有关。
类什么时候初始化:
加载完类后,类的初始化就会发生,意味着它会初始化所有类静态成员,以下情况一个类被初始化:
实例通过使用new()关键字创建或者使用class.forName()反射,但它有可能导致ClassNotFoundException。
类的静态方法被调用
类的静态域被赋值
静态域被访问,而且它不是常量
在顶层类中执行assert语句
反射同样可以使类初始化,比如java.lang.reflect包下面的某些方法。
类是如何被初始化的:
现在我们知道什么时候触发类的初始化了,他精确地写在Java语言规范中。但了解清楚 域(fields,静态的还是非静态的)、块(block静态的还是非静态的)、不同类(子类和超类)和不同的接口(子接口,实现类和超接口)的初始化顺序也很重要类。
下面是类初始化的一些规则:
类从顶至底的顺序初始化,所以声明在顶部的字段的早于底部的字段初始化
超类早于子类和衍生类的初始化
如果类的初始化是由于访问静态域而触发,那么只有声明静态域的类才被初始化,而不会触发超类的初始化或者子类的初始化即使静态域被子类或子接口或者它的实现类所引用。
接口初始化不会导致父接口的初始化。
静态域的初始化是在类的静态初始化期间,非静态域的初始化时在类的实例创建期间。这意味这静态域初始化在非静态域之前。
非静态域通过构造器初始化,子类在做任何初始化之前构造器会隐含地调用父类的构造器,他保证了非静态或实例变量(父类)初始化早于子类。
单例模式与类加载:
回到单例,饿汉模式属于立即加载模式在类一旦加载就会就会实例化单例对象。不管有没有使用到该单例类,都会导致单例对象在内存中存在,知道程序退出结束。如何理解这句话,看下面的代码:
单例类:
//饿汉式单例模式 public class Singleton { public static int MIN_USER = 10000; //私有构造函数防止外部创建对象 private Singleton(){ System.out.println("对象初始化"); } //静态对象对象初始化 private static Singleton singleton = new Singleton(); //静态工程方法 public static Singleton getInstance(){ return singleton; } public void doSomething(){ System.out.println(this.getClass().getName()); } public static void doSomethingStatic(){ System.out.println("这是一个静态方法"); } }
在这个类中添加了一个静态属性,一个静态方法。
调用:
public class SingletonTest { public static void main(String[] args){ Singleton singleton = null; System.out.println(singleton == null); } }
输出:
true Process finished with exit code 0
可以看到虽然引用了单例类,却没有生成单例对象。
访问静态属性调用:
public class SingletonTest { public static void main(String[] args){ int max_User = Singleton.MIN_USER; System.out.println(max_User); } }
输出:
对象初始化 10000 Process finished with exit code 0
如果为了像上述一样只为访问其某些静态属性或静态方法,却创建的单例对象。同理如果只调用doSomethingStatic()方法也会生成对象。
静态方法调用:
public class SingletonTest { public static void main(String[] args){ Singleton.doSomethingStatic(); } }
输出:
对象初始化
这是一个静态方法
Process finished with exit code 0
考虑一下final:
被final修饰的静态属性的访问不会触发实例化,被final修饰的静态方法仍然会触发实例化。被final修饰的类属性会被作为编译期常量加入常量池,以后访问对应类的常量池,不会在常量池中保存一个指向类字段的符号引用,不触发类的初始化。
总结:
饿汉模式是通过类的静态属性初始化来实现单例模式的实例化 private static Singleton singleton = new Singleton(); 立即加载在类完成初始化时也完成了单例对象的实例化。枚举方式也是同样的道理。
懒汉式(延时加载)是在显式调用 getInstance() 方法来完成单例对象的实例化,即类加载的七步中的使用阶段。
静态内部类能够实现延时加载是由于对于没有使用的类jvm是不会加载,即便其是一个内部类。其通过private修饰只能在外部类中访问。外部类中访问的唯一地方就是在 getInstance() 方法中。
public static Singleton5 getInstance(){ return InnerObjcet.singleton5; }
只有在触发内部类加载时才实例化单例对象。
为了便于理解用下面的代码理解:
单例类:
//静态内部类 public class Singleton5 { private Singleton5(){ System.out.println("对象实例化"); if(InnerObjcet.singleton5 != null){ throw new IllegalStateException(); } } private static class InnerObjcet{ static String str = "TEST"; private static Singleton5 singleton5 = new Singleton5(); static { System.out.println("内部类被加载"); } } public static Singleton5 getInstance(){ return InnerObjcet.singleton5; } public static void doSomethingStatic(){ System.out.println(InnerObjcet.str); } public void doSomething(){ System.out.println(this.getClass().getName()); } }
调用静态方法:
public class SingletonTest { public static void main(String[] args){ Singleton5.doSomethingStatic(); } }
输出:
对象实例化
内部类被加载
TEST
Process finished with exit code 0