我们先来看看网上普遍的结论:
所谓“懒汉式”与“饿汉式”的区别,是在与建立单例对象的时间的不同。
“懒汉式”是在你真正用到的时候才去建这个单例对象
“饿汉式是在类创建的同时就已经创建好一个静态的对象,不管你用的用不上,一开始就建立这个单例对象
先不说结论,看看下文
public class Singleton1 { private final static Singleton1 singleton = new Singleton(); private Singleton1() { System.out.println("饿汉式单例初始化!"); } public static Singleton1 getSingleton () { return singleton; } } 复制代码
在类静态变量里直接new一个单例
public class Singleton2 { private volatile static Singleton2 singleton; // 5 private Singleton2() { System.out.println("懒汉式单例初始化!"); } public static Singleton2 getInstance () { if(singleton ==null) { // 1 synchronized(Singleton2.class) { // 2 if(singleton == null) { // 3 singleton = new Singleton2(); //4 } } } return singleton; } } 复制代码
代码1 处的判空是为了减少同步方法的使用,提高效率
代码2,3 处的加锁和判空是为了防止多线程下重复实例化单例。
代码5 处的volatile是为了防止多线程下代码4 的指令重排序
创建一个Test测试类
public class Test { public static void main(String[] args) throws IOException { // 懒汉式 Singleton1 singleton1 = Singleton1.getInstance(); // 饿汉式 Singleton2 singleton2 = Singleton2.getInstance(); } } 复制代码
从结果上看没啥毛病,那我们来加个断点试试。按照以往的认知,饿汉单例是在类加载的时候的实例化,那么运行main方法应该会输出饿汉单例的初始化,我们来看看结果:
public static void main(String[] args) throws IOException { System.in.read(); // 饿汉式 Singleton1 singleton1 = Singleton1.getInstance(); // 懒汉式 Singleton2 singleton2 = Singleton2.getInstance(); } 复制代码
此时运行结果:
如图是没有结果的,饿汉单例怎么没有实例化呢?原来饿汉单例是在本类加载的时候才实例化的,在断点的时候还没有加载饿汉单例。 我们来详细复习一下类加载:
类的加载分为5个步骤: 加载、验证、准备、解析、初始化
初始化就是执行编译后的< cinit>()方法,而< cinit>()方法就是在编译时将静态变量赋值和静态块合并到一起生成的。
所以说,“饿汉模式”的创建对象是在类加载的初始化阶段进行的,那么类加载的初始化阶段在什么时候进行呢?jvm规范规定有且只有以下7种情况下会进行类加载的初始化阶段:
综上, 基本来说就是只有当你以某种方式调用了这个类的时候,它才会进行初始化,而不是jvm启动的时候就初始化,而jvm本身会确保类的初始化只执行一次 。那如果不使用这个单例对象的话,内存中根本没有Singleton实例对象,也就是和“懒汉模式”是 一样的效果 。
当然,也有一种可能就是单例类里除了getInstance()方法还有一些其他静态方法,这样当调用其他静态方法的时候,也会初始化实例,但是这个很容易解决,只要加个 内部类 就行了:
public class Singleton { private static class LazyHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance () { return LazyHolder.INSTANCE; } } 复制代码
网上的结论普遍说单例过早占用资源,而推荐使用“懒汉模式”,但他们忽略了单例何时进行类加载,经过以上分析,“懒汉模式”实现复杂而且没有任何独占优点, “饿汉模式”完胜 。“饿汉模式”使用场景推荐:
更新:
关于枚举类的:这里做个测试:
public enum SingletonEnum { INSTANCE; public SingletonEnum getInstance() { return INSTANCE; } SingletonEnum() { System.out.println("枚举类单例实例化啦"); } public static void test() { System.out.println("测试调用枚举类的静态方法"); } } 复制代码
测试类:
public static void main(String[] args) throws IOException { SingletonEnum.test(); System.in.read(); SingletonEnum singletonEnum=SingletonEnum.INSTANCE; } 复制代码
由此得出结论,枚举类的单例和普通的“饿汉模式”一样,都是在类加载(调用静态方法)的时候初始化。但是枚举类的另一个优点是能预防反射和序列化,因此再次得出结论