由一个单例所想到的

引言

最近关注了公众号“每日一题”,闲来看看,受益匪浅。机缘巧合,在CSDN找到作者的博客——工匠若水。其博文皆是精品,能将一个技术点讲解得全面透彻,本小虾米常常茅塞顿开,故记之。

一个“单例模式”引发的血案

下面这段代码是工匠给出的一个高效而且线程安全的懒汉式单例,其中system.out是我添加的日志。

package yanbober.github.io;
//懒汉模式(靠谱模式)
class DBManager {
    private static class InstanceHolder {
        static final DBManager INSTANCE = new DBManager();
    }

    private DBManager() {
        System.out.println("new a instance");
    }

    public static DBManager getInstance() {
        System.out.println("get a instance");
        return InstanceHolder.INSTANCE;
    }

    public boolean update() {
        System.out.println("update dbase success!");
        return true;
    }
}

public class Main {
    public static void main(String[] args) {
        DBManager dbManager = DBManager.getInstance();
        dbManager.update();
    }
}

上面一段代码的运行结果是什么?
我当时给出的答案是:
new a instance
get a instance
update dbase success!
但实际运行的结果却是:
get a instance
new a instance
update dbase success!
我想读到此处,也会有一些人给出与我相同的答案,因为这个单例怎么看都这么是一个饿汉式。

错误的分析

在基础的认识中,静态常量会在类加载的时候进行初始化,而类加载一般都发生在类的方法调用之前,所以

static final DBManager INSTANCE = new DBManager();

必然是最先执行的代码,所以最先打印的应该是“new a instance”。

正确的解释

上述分析确实有道理,但是忽略了一点,INSTANCE常亮并不是存在于DBManager类中,而是存在于它的内部类InstanceHolder中。现在我们分析一下Main这个程序入口类的main方法的执行过程。

public class Main {
    public static void main(String[] args) {
        DBManager dbManager = DBManager.getInstance();
        dbManager.update();
    }
}
  1. 声明一个变量DBManager dbManager,这时会对DBManager类进行初始化(初始化静态常量,运行静态代码块等)。注意:内部类InstanceHolder此时并没有进行初始化,内部类的加载过程和外部类是分开的,它们对于类加载机制来说是相同的东西。 我们可以认为,类并不是在JVM启动程序后就会执行加载,而且是在当前类被使用时才会进行加载和初始化。
  2. DBManager.getInstance(),调用了一个静态方法,顺序执行静态方法中的代码。
    public static DBManager getInstance() {
        System.out.println("get a instance");
        return InstanceHolder.INSTANCE;
    }
    
    1. 打印字符串“get a instance”。
    2. 返回静态变量InstanceHolder.INSTANCE,当程序执行到这一句时,第一次使用到类InstanceHolder,JVM才开始加载并初始化类InstanceHolder。初始化的过程中,需要对静态常量INSTANCE进行初始化,即通过new DBManager()实例化一个DBManager对象,此时才会调用DBManager类的构造方法,打印出字符串“new a instance”。
  3. 很简单,调用dbManager的方法update()打印字符串。

线程安全性分析

  1. 我们都知道饿汉式单例是线程安全的,因为单例对象初始化发生在类加载的时候,而类加载发生在线程使用单例之前,所以不会出现多个线程创建出多个单例对象的线程问题。
  2. 我们现在讨论的这个懒汉式单例的初始化也是发生在类加载的过程中,同理,也是一个线程安全的单例模式。
  3. 那么问题来了,这个懒汉式和饿汉式在使用上有什么不同呢?一般情况下,无论是效率,还是时间空间考虑上都无太大差异。分析如下:
    1. 我们都说懒汉式是时间换空间,饿汉式是空间换时间,但是我想说的是,既然我们使用的单例,那么空间已经是大大节省了。
    2. 至于懒汉式是否能真的通过运行时间换来不必要的空间开销,我觉得没有什么特别大的意义。无论是懒汉式还是饿汉式,从类加载的角度来看,他们的初始化过程都是在第一次调用getInstance方法时进行的,也就是说二者初始化的时间上基本一致。
    3. 那么我们今天这个看似很牛逼的高效线程安全的懒汉式真的没有什么意义吗?我只能说,比饿汉式的优势在于,如果这个单例模式中存在很多静态方法时,调用这些静态方法不会引起单例的初始化。但是单例模式中通常谁又会去写一堆静态方法在里面呢。
    4. 从这个角度来讲,一个相对简单的程序,饿汉式单例足矣。

你可能感兴趣的:(由一个单例所想到的)