别再用懒汉模式了——从JVM的角度看单例模式

·  阅读 2414

网上结论:

我们先来看看网上普遍的结论:

所谓“懒汉式”与“饿汉式”的区别,是在与建立单例对象的时间的不同。
“懒汉式”是在你真正用到的时候才去建这个单例对象
“饿汉式是在类创建的同时就已经创建好一个静态的对象,不管你用的用不上,一开始就建立这个单例对象

先不说结论,看看下文

代码实现:

饿汉式

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();
    }
}
复制代码

运行结果

image.png

从结果上看没啥毛病,那我们来加个断点试试。按照以往的认知,饿汉单例是在类加载的时候的实例化,那么运行main方法应该会输出饿汉单例的初始化,我们来看看结果:

public static void main(String[] args) throws IOException {
        System.in.read();
        // 饿汉式
        Singleton1 singleton1 = Singleton1.getInstance();
        // 懒汉式
        Singleton2 singleton2 = Singleton2.getInstance();
}
复制代码

此时运行结果:

image.png

如图是没有结果的,饿汉单例怎么没有实例化呢?原来饿汉单例是在本类加载的时候才实例化的,在断点的时候还没有加载饿汉单例。 我们来详细复习一下类加载:

类的加载分为5个步骤:加载、验证、准备、解析、初始化

初始化就是执行编译后的< cinit>()方法,而< cinit>()方法就是在编译时将静态变量赋值和静态块合并到一起生成的。

所以说,“饿汉模式”的创建对象是在类加载的初始化阶段进行的,那么类加载的初始化阶段在什么时候进行呢?jvm规范规定有且只有以下7种情况下会进行类加载的初始化阶段:

  • 使用new关键字实例化对象的时候
  • 设置或读取一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候
  • 调用一个类的静态方法的时候
  • 使用java.lang.reflect包的方法对类进行反射调用的时候
  • 初始化一个类的子类(会首先初始化父类)
  • 当虚拟机启动的时候,初始化包含main方法的主类
  • 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

  综上,基本来说就是只有当你以某种方式调用了这个类的时候,它才会进行初始化,而不是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;
    }
}
复制代码

总结

  网上的结论普遍说单例过早占用资源,而推荐使用“懒汉模式”,但他们忽略了单例何时进行类加载,经过以上分析,“懒汉模式”实现复杂而且没有任何独占优点,“饿汉模式”完胜。“饿汉模式”使用场景推荐:

  • 当单例类里有其他静态方法的时候,推荐使用静态内部类的形式。
  • 当单例类里只有getInstance()方法的时候,推荐直接new一个静态的单例对象。

更新:

关于枚举类的:这里做个测试:

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;

    }
复制代码

由此得出结论,枚举类的单例和普通的“饿汉模式”一样,都是在类加载(调用静态方法)的时候初始化。但是枚举类的另一个优点是能预防反射和序列化,因此再次得出结论

  • 当单例类里有其他静态方法的时候,推荐使用静态内部类的形式。
  • 当单例类里只有getInstance()方法的时候,推荐直接new一个静态的单例对象。
  • 当需要防止反射和序列化破坏单例的时候,推荐用枚举类的单例模式
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改